Use Stdin to pass secrets to ansible-playbook (#1911)

* feat: pass secrets via stdin

* feat: use pty

* feat(pty): logs

* feat(secrets): works

* fix(secrets): use correct ask flag of ansible playbook

* test(secrets): change tests
This commit is contained in:
Denis Gukov 2024-04-05 14:36:04 +02:00 committed by GitHub
parent 205fe71bcb
commit 3d571c0319
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 94 additions and 52 deletions

View File

@ -67,6 +67,8 @@ const (
type AccessKeyInstallation struct {
InstallationKey int64
SshAgent *lib.SshAgent
Login string
Password string
}
func (key AccessKeyInstallation) Destroy() error {
@ -115,8 +117,6 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat
return
}
installationPath := installation.GetPath()
err = key.DeserializeSecret()
if err != nil {
@ -132,44 +132,26 @@ func (key *AccessKey) Install(usage AccessKeyRole, logger lib.Logger) (installat
installation.SshAgent = &agent
}
case AccessKeyRoleAnsiblePasswordVault:
switch key.Type {
case AccessKeyLoginPassword:
err = os.WriteFile(installationPath, []byte(key.LoginPassword.Password), 0600)
}
case AccessKeyRoleAnsibleBecomeUser:
switch key.Type {
case AccessKeyLoginPassword:
content := make(map[string]string)
if len(key.LoginPassword.Login) > 0 {
content["ansible_become_user"] = key.LoginPassword.Login
}
content["ansible_become_password"] = key.LoginPassword.Password
var bytes []byte
bytes, err = json.Marshal(content)
if err != nil {
return
}
err = os.WriteFile(installationPath, bytes, 0600)
default:
if key.Type != AccessKeyLoginPassword {
err = fmt.Errorf("access key type not supported for ansible user")
}
installation.Password = key.LoginPassword.Password
case AccessKeyRoleAnsibleBecomeUser:
if key.Type != AccessKeyLoginPassword {
err = fmt.Errorf("access key type not supported for ansible user")
}
installation.Login = key.LoginPassword.Login
installation.Password = key.LoginPassword.Password
case AccessKeyRoleAnsibleUser:
switch key.Type {
case AccessKeySSH:
var agent lib.SshAgent
agent, err = key.startSshAgent(logger)
installation.SshAgent = &agent
installation.Login = key.LoginPassword.Login
case AccessKeyLoginPassword:
content := make(map[string]string)
content["ansible_user"] = key.LoginPassword.Login
content["ansible_password"] = key.LoginPassword.Password
var bytes []byte
bytes, err = json.Marshal(content)
if err != nil {
return
}
err = os.WriteFile(installationPath, bytes, 0600)
installation.Login = key.LoginPassword.Login
installation.Password = key.LoginPassword.Password
default:
err = fmt.Errorf("access key type not supported for ansible user")
}

View File

@ -60,8 +60,8 @@ func (t *AnsibleApp) SetLogger(logger lib.Logger) lib.Logger {
return logger
}
func (t *AnsibleApp) Run(args []string, environmentVars *[]string, cb func(*os.Process)) error {
return t.Playbook.RunPlaybook(args, environmentVars, cb)
func (t *AnsibleApp) Run(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error {
return t.Playbook.RunPlaybook(args, environmentVars, inputs, cb)
}
func (t *AnsibleApp) Log(msg string) {

View File

@ -5,6 +5,7 @@ import (
"github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/lib"
"github.com/ansible-semaphore/semaphore/util"
"github.com/creack/pty"
"os"
"os/exec"
"strings"
@ -53,14 +54,41 @@ func (p AnsiblePlaybook) runCmd(command string, args []string) error {
return cmd.Run()
}
func (p AnsiblePlaybook) RunPlaybook(args []string, environmentVars *[]string, cb func(*os.Process)) error {
func (p AnsiblePlaybook) RunPlaybook(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error {
cmd := p.makeCmd("ansible-playbook", args, environmentVars)
p.Logger.LogCmd(cmd)
cmd.Stdin = strings.NewReader("")
err := cmd.Start()
ptmx, err := pty.Start(cmd)
if err != nil {
return err
panic(err)
}
go func() {
b := make([]byte, 100)
var e error
for {
var n int
n, e = ptmx.Read(b)
if e != nil {
break
}
s := strings.TrimSpace(string(b[0:n]))
for k, v := range inputs {
if strings.HasPrefix(s, k) {
_, _ = ptmx.WriteString(v + "\n")
}
}
}
}()
defer func() { _ = ptmx.Close() }()
cb(cmd.Process)
return cmd.Wait()
}

View File

@ -8,5 +8,5 @@ import (
type LocalApp interface {
SetLogger(logger lib.Logger) lib.Logger
InstallRequirements() error
Run(args []string, environmentVars *[]string, cb func(*os.Process)) error
Run(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.21
require (
github.com/Masterminds/squirrel v1.5.4
github.com/coreos/go-oidc/v3 v3.9.0
github.com/creack/pty v1.1.21
github.com/go-git/go-git/v5 v5.11.0
github.com/go-gorp/gorp/v3 v3.1.0
github.com/go-ldap/ldap/v3 v3.4.6

2
go.sum
View File

@ -22,6 +22,8 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -175,7 +175,11 @@ func (t *LocalJob) getTerraformArgs(username string, incomingVersion *string) (a
}
// nolint: gocyclo
func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (args []string, err error) {
func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (args []string, inputs map[string]string, err error) {
inputMap := make(map[db.AccessKeyRole]string)
inputs = make(map[string]string)
playbookName := t.Task.Playbook
if playbookName == "" {
playbookName = t.Template.Playbook
@ -202,12 +206,17 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar
if t.Inventory.SSHKeyID != nil {
switch t.Inventory.SSHKey.Type {
case db.AccessKeySSH:
//args = append(args, "--extra-vars={\"ansible_ssh_private_key_file\": \""+t.inventory.SSHKey.GetPath()+"\"}")
if t.Inventory.SSHKey.SshKey.Login != "" {
args = append(args, "--extra-vars={\"ansible_user\": \""+t.Inventory.SSHKey.SshKey.Login+"\"}")
if t.sshKeyInstallation.Login != "" {
args = append(args, "--user", t.sshKeyInstallation.Login)
}
case db.AccessKeyLoginPassword:
args = append(args, "--extra-vars=@"+t.sshKeyInstallation.GetPath())
if t.sshKeyInstallation.Login != "" {
args = append(args, "--user", t.sshKeyInstallation.Login)
}
if t.sshKeyInstallation.Password != "" {
args = append(args, "--ask-pass")
inputMap[db.AccessKeyRoleAnsibleUser] = t.sshKeyInstallation.Password
}
case db.AccessKeyNone:
default:
err = fmt.Errorf("access key does not suite for inventory's user credentials")
@ -218,7 +227,13 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar
if t.Inventory.BecomeKeyID != nil {
switch t.Inventory.BecomeKey.Type {
case db.AccessKeyLoginPassword:
args = append(args, "--extra-vars=@"+t.becomeKeyInstallation.GetPath())
if t.sshKeyInstallation.Login != "" {
args = append(args, "--user", t.becomeKeyInstallation.Login)
}
if t.becomeKeyInstallation.Password != "" {
args = append(args, "--ask-become-pass")
inputMap[db.AccessKeyRoleAnsibleBecomeUser] = t.sshKeyInstallation.Password
}
case db.AccessKeyNone:
default:
err = fmt.Errorf("access key does not suite for inventory's sudo user credentials")
@ -239,7 +254,8 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar
}
if t.Template.VaultKeyID != nil {
args = append(args, "--vault-password-file", t.vaultFileInstallation.GetPath())
args = append(args, "--ask-vault-pass")
inputMap[db.AccessKeyRoleAnsiblePasswordVault] = t.vaultFileInstallation.Password
}
extraVars, err := t.getEnvironmentExtraVarsJSON(username, incomingVersion)
@ -277,6 +293,18 @@ func (t *LocalJob) getPlaybookArgs(username string, incomingVersion *string) (ar
args = append(args, taskExtraArgs...)
args = append(args, playbookName)
if line, ok := inputMap[db.AccessKeyRoleAnsibleUser]; ok {
inputs["SSH password:"] = line
}
if line, ok := inputMap[db.AccessKeyRoleAnsibleBecomeUser]; ok {
inputs["BECOME password"] = line
}
if line, ok := inputMap[db.AccessKeyRoleAnsiblePasswordVault]; ok {
inputs["Vault password:"] = line
}
return
}
@ -294,10 +322,11 @@ func (t *LocalJob) Run(username string, incomingVersion *string) (err error) {
}()
var args []string
var inputs map[string]string
switch t.Template.App {
case db.TemplateAnsible:
args, err = t.getPlaybookArgs(username, incomingVersion)
args, inputs, err = t.getPlaybookArgs(username, incomingVersion)
default:
panic("unknown template app")
}
@ -315,7 +344,7 @@ func (t *LocalJob) Run(username string, incomingVersion *string) (err error) {
environmentVariables = append(environmentVariables, fmt.Sprintf("SSH_AUTH_SOCK=%s", t.sshKeyInstallation.SshAgent.SocketFile))
}
return t.App.Run(args, &environmentVariables, func(p *os.Process) {
return t.App.Run(args, &environmentVariables, inputs, func(p *os.Process) {
t.Process = p
})

View File

@ -299,7 +299,7 @@ func TestTaskGetPlaybookArgs(t *testing.T) {
},
}
args, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil)
args, _, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil)
if err != nil {
t.Fatal(err)
@ -355,14 +355,14 @@ func TestTaskGetPlaybookArgs2(t *testing.T) {
},
}
args, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil)
args, _, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil)
if err != nil {
t.Fatal(err)
}
res := strings.Join(args, " ")
if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" {
if res != "-i /tmp/inventory_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" {
t.Fatal("incorrect result")
}
}
@ -411,14 +411,14 @@ func TestTaskGetPlaybookArgs3(t *testing.T) {
},
}
args, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil)
args, _, err := tsk.job.(*LocalJob).getPlaybookArgs("", nil)
if err != nil {
t.Fatal(err)
}
res := strings.Join(args, " ")
if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" {
if res != "-i /tmp/inventory_0 --extra-vars {\"semaphore_vars\":{\"task_details\":{\"id\":0,\"username\":\"\"}}} test.yml" {
t.Fatal("incorrect result")
}
}