diff --git a/README.md b/README.md index 47f4d59..ea95e06 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,12 @@ if multiple keys are used to encrypt app secrets then this secret should contain ### `generate` command will run kustomize build to generate kube resources's yaml strings. it will print this yaml stream to stdout. -You can specify custom SSH keys to be used for fetching remote kustomize bases from private repositories. In order to do that, -you will need to set `GIT_SSH_SECRET_NAME` plugin env, it should reference a Secret name that contains one or more SSH keys -that provide access to the private repositories that contain these bases. if this env is not set then plugin will -only be able to fetch remote bases from open repositories. +Plugin support fetching remote base from private repositories, to do that user must create a secret with name `argocd-voodoobox-git-ssh`, +that contains one or more SSH keys that provide access to the private repositories that contain these bases. To use an SSH key for Kustomize bases, +the bases URL should be defined with the ssh:// scheme in kustomization.yaml and have a `# argocd-voodoobox-plugin: ` comment above it. +if only 1 ssh key is used for ALL private repos then there is no need to specify this comment. -To use an SSH key for Kustomize bases, the bases should be defined with the ssh:// scheme in kustomization.yaml and have a -`# argocd-voodoobox-plugin: key_foobar` comment above it. For example: +If `ssh://` is not used then plugin will assume only public repos are used and it will skip ssh config setup. ```yaml resources: @@ -39,7 +38,7 @@ resources: Plugin supports following _plugin envs_ which can be set in ArgoCD Application crd -`STRONGBOX_SECRET_NAME` the value should be the name of a secret resource containing strongbox keyring used to encrypt app secrets. the default value is `argocd-strongbox-keyring` +The value of name of a secret resource containing strongbox keyring used to encrypt app secrets, must be `argocd-voodoobox-strongbox-keyring`. `STRONGBOX_SECRET_KEY` the value should be the name of the secret data key which contains a valid strongbox keyring file data. the default value is `.strongbox_keyring` @@ -54,7 +53,7 @@ If this env is not specified then it defaults to the same namespace as the app's kind: Secret apiVersion: v1 metadata: - name: argocd-strongbox-keyring + name: argocd-voodoobox-strongbox-keyring namespace: ns-a annotations: argocd.voodoobox.plugin.io/allowed-namespaces: "ns-b, ns-c" @@ -76,17 +75,12 @@ spec: env: - name: STRONGBOX_SECRET_NAMESPACE value: team-a - - name: STRONGBOX_SECRET_NAME - value: argocd-strongbox-keyring - name: STRONGBOX_SECRET_KEY value: .strongbox_keyring ``` ### Git SSH Keys Envs -`GIT_SSH_SECRET_NAME` the value should be the name of a secret resource containing ssh keys used for fetching remote kustomize bases from private repositories. Additionally this Secret can optionally define a value for "known_hosts". If omitted, git will use ssh with StrictHostKeyChecking disabled. There is no default value for this env it must be set if repo base contains remote -private bases. - `GIT_SSH_SECRET_NAMESPACE` the value should be the name of a namespace where secret resource containing ssh keys are located. If this env is not specified then it defaults to the same namespace as the app's destination NS. the Secret should have an annotation called "argocd.voodoobox.plugin.io/allowed-namespaces" which contains a comma-separated list of all the namespaces that are allowed to use it. @@ -94,7 +88,7 @@ the Secret should have an annotation called "argocd.voodoobox.plugin.io/allowed- kind: Secret apiVersion: v1 metadata: - name: argocd-git-ssh + name: argocd-voodoobox-git-ssh namespace: ns-a annotations: kube-applier.io/allowed-namespaces: "ns-b, ns-c" @@ -139,12 +133,14 @@ data: - argocd-voodoobox-plugin - decrypt args: + - "--app-strongbox-secret-name=argocd-voodoobox-strongbox-keyring" - "--secret-allowed-namespaces-annotation=argocd.voodoobox.plugin.io/allowed-namespaces" generate: command: - argocd-voodoobox-plugin - generate args: + - "--app-git-ssh-secret-name=argocd-voodoobox-git-ssh" - "--secret-allowed-namespaces-annotation=argocd.voodoobox.plugin.io/allowed-namespaces" lockRepo: false ``` @@ -204,6 +200,9 @@ metadata: rules: - apiGroups: [""] resources: ["secrets"] + resourceNames: + - argocd-voodoobox-strongbox-keyring + - argocd-voodoobox-git-ssh verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 @@ -230,10 +229,12 @@ subjects: | app arguments/ENVs | default | example / explanation | |-|-|-| | --secret-allowed-namespaces-annotation | argocd.voodoobox.plugin.io/allowed-namespaces | when shared secret is used this value is the annotation key to look for in secret to get comma-separated list of all the namespaces that are allowed to use it | +| --app-strongbox-secret-name | argocd-voodoobox-strongbox-keyring | the value should be the name of a secret resource containing strongbox keyring used to encrypt app secrets. name will be same across all applications | +| --app-git-ssh-secret-name | argocd-voodoobox-git-ssh | the value should be the name of a secret resource containing ssh keys used for fetching remote kustomize bases from private repositories. name will be same across all applications | | ARGOCD_APP_NAME | set by argocd | name of application | | ARGOCD_APP_NAMESPACE | set by argocd | application's destination namespace | -| STRONGBOX_SECRET_NAME¹ | argocd-strongbox-keyring | the name of a secret resource containing strongbox keyring used to encrypt app secrets | | STRONGBOX_KEYRING_KEY¹ | .strongbox_keyring | the name of the secret data key which contains a valid strongbox keyring file | | STRONGBOX_SECRET_NAMESPACE¹ | | the name of a namespace where secret resource containing strongbox keyring is located | +| GIT_SSH_SECRET_NAMESPACE¹ | | the value should be the name of a namespace where secret resource containing ssh keys are located | ¹ These ENVs should be added to argocd application plugin env sections diff --git a/generate.go b/generate.go index c16150c..b85d500 100644 --- a/generate.go +++ b/generate.go @@ -4,21 +4,47 @@ import ( "bytes" "context" "fmt" + "io/fs" "os" "os/exec" + "path/filepath" "strings" ) func ensureBuild(ctx context.Context, cwd string, app applicationInfo) (string, error) { - c, err := setupGitSSH(ctx, cwd, app) + // Even when there is no git SSH secret defined, we still override the + // git ssh command (pointing the key to /dev/null) in order to avoid + // using ssh keys in default system locations and to surface the error + // if bases over ssh have been configured. + sshCmdEnv := `GIT_SSH_COMMAND=ssh -q -F none -o IdentitiesOnly=yes -o IdentityFile=/dev/null -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no` + + kFiles, err := findKustomizeFiles(cwd) if err != nil { - return "", err + return "", fmt.Errorf("unable to ge kustomize files paths err:%s", err) + } + + if len(kFiles) == 0 { + return "", fmt.Errorf("only kustomize base is supported") } - // - env := os.Environ() + hasRemoteBase, err := hasSSHRemoteBaseURL(kFiles) + if err != nil { + return "", fmt.Errorf("unable to look for ssh protocol err:%s", err) + } + + if hasRemoteBase { + sshCmdEnv, err = setupGitSSH(ctx, cwd, app) + if err != nil { + return "", err + } + } + + // setup env for Kustomize command + env := []string{ + fmt.Sprintf("PATH=%s", os.Getenv("PATH")), + } - env = append(env, c) + env = append(env, sshCmdEnv) // Set HOME to cwd, this means that SSH should not pick up any // local SSH keys and use them for cloning env = append(env, fmt.Sprintf("HOME=%s", cwd)) @@ -26,6 +52,37 @@ func ensureBuild(ctx context.Context, cwd string, app applicationInfo) (string, return runKustomizeBuild(ctx, cwd, env) } +func findKustomizeFiles(cwd string) ([]string, error) { + kFiles := []string{} + + err := filepath.WalkDir(cwd, func(path string, info fs.DirEntry, err error) error { + if filepath.Base(path) == "kustomization.yaml" || + filepath.Base(path) == "kustomization.yml" || + filepath.Base(path) == "Kustomization" { + kFiles = append(kFiles, path) + } + return nil + }) + if err != nil { + return nil, err + } + + return kFiles, nil +} + +func hasSSHRemoteBaseURL(kFiles []string) (bool, error) { + for _, k := range kFiles { + data, err := os.ReadFile(k) + if err != nil { + return false, err + } + if bytes.Contains(data, []byte("ssh://")) { + return true, nil + } + } + return false, nil +} + // runKustomizeBuild will run `kustomize build` cmd and return generated yaml or error func runKustomizeBuild(ctx context.Context, cwd string, env []string) (string, error) { k := exec.CommandContext(ctx, "kustomize", "build", ".") diff --git a/generate_test.go b/generate_test.go new file mode 100644 index 0000000..2d67aa6 --- /dev/null +++ b/generate_test.go @@ -0,0 +1,35 @@ +package main + +import "testing" + +func Test_hasSSHRemoteBaseURL(t *testing.T) { + type args struct { + cwd string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + {"with remote base", args{"./testData/app-with-remote-base"}, true, false}, + {"without remote base", args{"./testData/app-with-secrets"}, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + files, err := findKustomizeFiles(tt.args.cwd) + if err != nil { + t.Fatal(err) + } + got, err := hasSSHRemoteBaseURL(files) + if (err != nil) != tt.wantErr { + t.Errorf("hasSSHRemoteBaseURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("hasSSHRemoteBaseURL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/git-ssh.go b/git-ssh.go index f46a671..d00e838 100644 --- a/git-ssh.go +++ b/git-ssh.go @@ -33,26 +33,16 @@ var ( ) func setupGitSSH(ctx context.Context, cwd string, app applicationInfo) (string, error) { - // Even when there is no git SSH secret defined, we still override the - // git ssh command (pointing the key to /dev/null) in order to avoid - // using ssh keys in default system locations and to surface the error - // if bases over ssh have been configured. - sshCmd := `GIT_SSH_COMMAND=ssh -q -F none -o IdentitiesOnly=yes -o IdentityFile=/dev/null -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no` knownHostsFragment := `-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no` - // if ssh secret name is not set skip setup - if app.gitSSHSecret.name == "" { - return sshCmd, nil - } - sec, err := getSecret(ctx, app.destinationNamespace, app.gitSSHSecret) if err != nil { - return sshCmd, fmt.Errorf("unable to get secret err:%v", err) + return "", fmt.Errorf("unable to get secret err:%v", err) } sshDir := filepath.Join(cwd, ".ssh") if err := os.Mkdir(sshDir, 0700); err != nil { - return sshCmd, fmt.Errorf("unable to create ssh config dir err:%s", err) + return "", fmt.Errorf("unable to create ssh config dir err:%s", err) } // keyFilePaths holds key name and path values diff --git a/git-ssh_test.go b/git-ssh_test.go index 7f06cb9..260b74d 100644 --- a/git-ssh_test.go +++ b/git-ssh_test.go @@ -338,7 +338,7 @@ func Test_setupGitSSH(t *testing.T) { kubeClient = fake.NewSimpleClientset( &v1.Secret{ ObjectMeta: metaV1.ObjectMeta{ - Name: "argocd-git-ssh", + Name: "argocd-voodoobox-git-ssh", Namespace: "foo", }, Data: map[string][]byte{ @@ -352,30 +352,28 @@ func Test_setupGitSSH(t *testing.T) { }, ) - noRemoteBase := applicationInfo{ + withOutSecret := applicationInfo{ name: "app-foo", - destinationNamespace: "foo", - } - - defaultEnv := "GIT_SSH_COMMAND=ssh -q -F none -o IdentitiesOnly=yes -o IdentityFile=/dev/null -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" - env, err := setupGitSSH(context.Background(), withRemoteBaseTestDir, noRemoteBase) - if err != nil { - t.Fatal(err) + destinationNamespace: "foo-bar", + gitSSHSecret: secretInfo{ + name: "argocd-voodoobox-git-ssh", + }, } - if diff := cmp.Diff(defaultEnv, env); diff != "" { - t.Errorf("setupGitSSH() mismatch (-want +got):\n%s", diff) + _, err := setupGitSSH(context.Background(), withRemoteBaseTestDir, withOutSecret) + if err == nil { + t.Fatal("expecting error here for missing secret from foo-bar NS") } app := applicationInfo{ name: "app-foo", destinationNamespace: "foo", gitSSHSecret: secretInfo{ - name: "argocd-git-ssh", + name: "argocd-voodoobox-git-ssh", }, } wnatEnv := "GIT_SSH_COMMAND=ssh -q -F testData/app-with-remote-base-test1/.ssh/config -o UserKnownHostsFile=testData/app-with-remote-base-test1/.ssh/known_hosts" - env, err = setupGitSSH(context.Background(), withRemoteBaseTestDir, app) + env, err := setupGitSSH(context.Background(), withRemoteBaseTestDir, app) if err != nil { t.Fatal(err) } diff --git a/main.go b/main.go index 3d8461b..9ddcf24 100644 --- a/main.go +++ b/main.go @@ -74,13 +74,6 @@ func main() { Usage: `set 'STRONGBOX_SECRET_NAMESPACE' in argocd application as plugin ENV. the value should be the name of a namespace where secret resource containing strongbox keyring is located`, }, - &cli.StringFlag{ - Name: "app-strongbox-secret-name", - EnvVars: []string{argocdAppEnvPrefix + "STRONGBOX_SECRET_NAME"}, - Usage: `set 'STRONGBOX_SECRET_NAME' in argocd application as plugin ENV. the value should be the - name of a secret resource containing strongbox keyring used to encrypt app secrets`, - Value: "argocd-strongbox-keyring", - }, &cli.StringFlag{ Name: "app-strongbox-secret-key", EnvVars: []string{argocdAppEnvPrefix + "STRONGBOX_SECRET_KEY"}, @@ -88,6 +81,15 @@ func main() { name of the secret data key which contains a valid strongbox keyring file`, Value: ".strongbox_keyring", }, + // do not set `EnvVars` for secret name flag + // To keep service account's permission minimum, the name of the secret is static across ALL applications. + // this value should only be set by admins of argocd as part of plugin setup + &cli.StringFlag{ + Name: "app-strongbox-secret-name", + Usage: `the value should be the name of a secret resource containing strongbox keyring used to +encrypt app secrets. name will be same across all applications`, + Value: "argocd-voodoobox-strongbox-keyring", + }, &cli.StringFlag{ Name: "secret-allowed-namespaces-annotation", Usage: `when shared secret is used this value is the annotation key to look for in secret @@ -133,11 +135,14 @@ func main() { Usage: `set 'GIT_SSH_SECRET_NAMESPACE' in argocd application as plugin ENV. the value should be the name of a namespace where secret resource containing ssh keys are located`, }, + // do not set `EnvVars` for secret name flag + // To keep service account's permission minimum, the name of the secret is static across ALL applications. + // this value should only be set by admins of argocd as part of plugin setup &cli.StringFlag{ - Name: "app-git-ssh-secret-name", - EnvVars: []string{argocdAppEnvPrefix + "GIT_SSH_SECRET_NAME"}, - Usage: `set 'GIT_SSH_SECRET_NAME' in argocd application as plugin ENV. the value should be the - name of a secret resource containing ssh keys used for fetching remote kustomize bases from private repositories`, + Name: "app-git-ssh-secret-name", + Usage: `the value should be the name of a secret resource containing ssh keys used for +fetching remote kustomize bases from private repositories. name will be same across all applications`, + Value: "argocd-voodoobox-git-ssh", }, &cli.StringFlag{ Name: "secret-allowed-namespaces-annotation",