diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 642a1bc4..66286858 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -2,6 +2,9 @@ aeece Artifactory applicationid +atlassian +Bitbucket +bitbucketserver bacd CVE credref @@ -11,11 +14,14 @@ eec fbd ffb gitlab +GPG helmvalues +html installationid jfrog mep myregistry +openpgp PRIVATEKEYDATA repocreds rollbacked diff --git a/cmd/main.go b/cmd/main.go index de6f0c69..81eed761 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,25 +28,28 @@ const applicationsAPIKindArgoCD = "argocd" // ImageUpdaterConfig contains global configuration and required runtime data type ImageUpdaterConfig struct { - ApplicationsAPIKind string - ClientOpts argocd.ClientOptions - ArgocdNamespace string - DryRun bool - CheckInterval time.Duration - ArgoClient argocd.ArgoCD - LogLevel string - KubeClient *kube.KubernetesClient - MaxConcurrency int - HealthPort int - MetricsPort int - RegistriesConf string - AppNamePatterns []string - AppLabel string - GitCommitUser string - GitCommitMail string - GitCommitMessage *template.Template - DisableKubeEvents bool - GitCreds git.CredsStore + ApplicationsAPIKind string + ClientOpts argocd.ClientOptions + ArgocdNamespace string + DryRun bool + CheckInterval time.Duration + ArgoClient argocd.ArgoCD + LogLevel string + KubeClient *kube.KubernetesClient + MaxConcurrency int + HealthPort int + MetricsPort int + RegistriesConf string + AppNamePatterns []string + AppLabel string + GitCommitUser string + GitCommitMail string + GitCommitMessage *template.Template + GitCommitSigningKey string + GitCommitSigningMethod string + GitCommitSignOff bool + DisableKubeEvents bool + GitCreds git.CredsStore } // newRootCommand implements the root command of argocd-image-updater diff --git a/cmd/run.go b/cmd/run.go index bcc0e1fe..751c29c9 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -240,6 +240,9 @@ func newRunCommand() *cobra.Command { runCmd.Flags().BoolVar(&warmUpCache, "warmup-cache", true, "whether to perform a cache warm-up on startup") runCmd.Flags().StringVar(&cfg.GitCommitUser, "git-commit-user", env.GetStringVal("GIT_COMMIT_USER", "argocd-image-updater"), "Username to use for Git commits") runCmd.Flags().StringVar(&cfg.GitCommitMail, "git-commit-email", env.GetStringVal("GIT_COMMIT_EMAIL", "noreply@argoproj.io"), "E-Mail address to use for Git commits") + runCmd.Flags().StringVar(&cfg.GitCommitSigningKey, "git-commit-signing-key", env.GetStringVal("GIT_COMMIT_SIGNING_KEY", ""), "GnuPG key ID or path to Private SSH Key used to sign the commits") + runCmd.Flags().StringVar(&cfg.GitCommitSigningMethod, "git-commit-signing-method", env.GetStringVal("GIT_COMMIT_SIGNING_METHOD", "openpgp"), "Method used to sign Git commits ('openpgp' or 'ssh')") + runCmd.Flags().BoolVar(&cfg.GitCommitSignOff, "git-commit-sign-off", env.GetBoolVal("GIT_COMMIT_SIGN_OFF", false), "Whether to sign-off git commits") runCmd.Flags().StringVar(&commitMessagePath, "git-commit-message-path", defaultCommitTemplatePath, "Path to a template to use for Git commit messages") runCmd.Flags().BoolVar(&cfg.DisableKubeEvents, "disable-kube-events", env.GetBoolVal("IMAGE_UPDATER_KUBE_EVENTS", false), "Disable kubernetes events") @@ -319,16 +322,19 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR defer sem.Release(1) log.Debugf("Processing application %s", app) upconf := &argocd.UpdateConfiguration{ - NewRegFN: registry.NewClient, - ArgoClient: cfg.ArgoClient, - KubeClient: cfg.KubeClient, - UpdateApp: &curApplication, - DryRun: dryRun, - GitCommitUser: cfg.GitCommitUser, - GitCommitEmail: cfg.GitCommitMail, - GitCommitMessage: cfg.GitCommitMessage, - DisableKubeEvents: cfg.DisableKubeEvents, - GitCreds: cfg.GitCreds, + NewRegFN: registry.NewClient, + ArgoClient: cfg.ArgoClient, + KubeClient: cfg.KubeClient, + UpdateApp: &curApplication, + DryRun: dryRun, + GitCommitUser: cfg.GitCommitUser, + GitCommitEmail: cfg.GitCommitMail, + GitCommitMessage: cfg.GitCommitMessage, + GitCommitSigningKey: cfg.GitCommitSigningKey, + GitCommitSigningMethod: cfg.GitCommitSigningMethod, + GitCommitSignOff: cfg.GitCommitSignOff, + DisableKubeEvents: cfg.DisableKubeEvents, + GitCreds: cfg.GitCreds, } res := argocd.UpdateApplication(upconf, syncState) result.NumApplicationsProcessed += 1 diff --git a/docs/basics/update-methods.md b/docs/basics/update-methods.md index 404b5aa4..73d82bf7 100644 --- a/docs/basics/update-methods.md +++ b/docs/basics/update-methods.md @@ -247,6 +247,58 @@ as the author. You can override the author using the `git.user` and `git.email` in the `argocd-image-updater-config` ConfigMap. +## Enabling commit signature signing using an SSH or GPG key + +### 1. SCM branch protection rules require signed commits +Commit signing for SCM branch protection rules require the repository be accessed using HTTPS or SSH with a user account. +Repositories accessed using a GitHub App can not be verified when using the git command line at this time. + +Each Git commit associated with an author's name and email address can be signed via a private SSH key or GPG key. + +Commit signing requires a bot account with a GPG or SSH key and the username and email address configured to match the bot account. + +Your preferred signing key must be associated with your bot account. See SCM provider documentation for further details: +* [GitHub](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) +* [GitLab](https://docs.gitlab.com/ee/user/project/repository/signed_commits/) +* [Bitbucket](https://confluence.atlassian.com/bitbucketserver/controlling-access-to-code-776639770.html) + +### 2. Signing commits for future use with ArgoCD Source Verification Policies +Commits can also be signed for use with source verification. +In this case signing keys do not need to be associated with an SCM user account. + +**SSH:** + +The private key must be mounted and accessible on the `argocd-image-updater` pod. + +Set `git.commit-signing-key` `argocd-image-updater-config` ConfigMap to the path of your private key: + +```yaml +data: + git.commit-sign-off: "true" + git.commit-signing-key: /app/.ssh/id_rsa + git.commit-signing-method: "ssh" +``` + +Create a new SSH secret or use your existing SSH secret: +```bash +kubectl -n argocd-image-updater create secret generic ssh-git-creds \ + --from-file=sshPrivateKey=~/.ssh/id_rsa +``` + +**GPG:** + +The GPG private key must be installed and available in the `argocd-image-updater` pod. +The `git.commit-signing-method` defaults to `openpgp`. +Set `git.commit-signing-key` in the `argocd-image-updater-config` ConfigMap to the GPG key ID you want to use: + +```yaml +data: + git.commit-sign-off: "true" + git.commit-signing-key: 3AA5C34371567BD2 +``` + +#### Commit Sign Off can be enabled by setting `git.commit-sign-off: "true"` + ### Changing the Git commit message You can change the default commit message used by Argo CD Image Updater to some diff --git a/ext/git/mocks/Client.go b/ext/git/mocks/Client.go index 63252281..87bd3b46 100644 --- a/ext/git/mocks/Client.go +++ b/ext/git/mocks/Client.go @@ -160,6 +160,20 @@ func (_m *Client) Config(username string, email string) error { return r0 } +// SigningConfig provides a mock function with given fields: signingkey +func (_m *Client) SigningConfig(signingkey string) error { + ret := _m.Called(signingkey) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(signingkey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Fetch provides a mock function with given fields: revision func (_m *Client) Fetch(revision string) error { ret := _m.Called(revision) diff --git a/ext/git/writer.go b/ext/git/writer.go index 7b84ff92..94535aec 100644 --- a/ext/git/writer.go +++ b/ext/git/writer.go @@ -14,8 +14,10 @@ type CommitOptions struct { CommitMessageText string // CommitMessagePath holds the path to a file to be used for the commit message (-F option) CommitMessagePath string - // SigningKey holds a GnuPG key ID used to sign the commit with (-S option) + // SigningKey holds a GnuPG key ID or path to a Private SSH Key used to sign the commit with (-S option) SigningKey string + // SigningMethod holds the signing method used to sign commits. (git -c gpg.format=ssh option) + SigningMethod string // SignOff specifies whether to sign-off a commit (-s option) SignOff bool } @@ -25,16 +27,18 @@ type CommitOptions struct { // changes will be commited. If message is not the empty string, it will be // used as the commit message, otherwise a default commit message will be used. // If signingKey is not the empty string, commit will be signed with the given -// GPG key. +// GPG or SSH key. func (m *nativeGitClient) Commit(pathSpec string, opts *CommitOptions) error { defaultCommitMsg := "Update parameters" - args := []string{"commit"} + // Git configuration + config := "gpg.format=" + opts.SigningMethod + args := []string{"-c", config, "commit"} if pathSpec == "" || pathSpec == "*" { args = append(args, "-a") } - if opts.SigningKey != "" { - args = append(args, "-S", opts.SigningKey) - } + // Commit fails with a space between -S flag and path to SSH key + // -S/user/test/.ssh/signingKey or -SAAAAAAAA... + args = append(args, fmt.Sprintf("-S%s", opts.SigningKey)) if opts.SignOff { args = append(args, "-s") } diff --git a/manifests/base/deployment/argocd-image-updater-deployment.yaml b/manifests/base/deployment/argocd-image-updater-deployment.yaml index 5682ed78..24bd7ced 100644 --- a/manifests/base/deployment/argocd-image-updater-deployment.yaml +++ b/manifests/base/deployment/argocd-image-updater-deployment.yaml @@ -77,6 +77,24 @@ spec: name: argocd-image-updater-config key: git.email optional: true + - name: GIT_COMMIT_SIGNING_KEY + valueFrom: + configMapKeyRef: + key: git.commit-signing-key + name: argocd-image-updater-config + optional: true + - name: GIT_COMMIT_SIGNING_METHOD + valueFrom: + configMapKeyRef: + key: git.commit-signing-method + name: argocd-image-updater-config + optional: true + - name: GIT_COMMIT_SIGN_OFF + valueFrom: + configMapKeyRef: + key: git.commit-sign-off + name: argocd-image-updater-config + optional: true - name: IMAGE_UPDATER_KUBE_EVENTS valueFrom: configMapKeyRef: @@ -116,6 +134,10 @@ spec: name: ssh-config - mountPath: /tmp name: tmp + - name: ssh-signing-key + mountPath: /app/.ssh/id_rsa + readOnly: true + subPath: sshPrivateKey serviceAccountName: argocd-image-updater volumes: - configMap: @@ -135,5 +157,9 @@ spec: name: argocd-image-updater-ssh-config optional: true name: ssh-config + - name: ssh-signing-key + secret: + secretName: ssh-git-creds + optional: true - emptyDir: {} name: tmp diff --git a/manifests/install.yaml b/manifests/install.yaml index b65b12bd..97167ec0 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -158,6 +158,24 @@ spec: key: git.email name: argocd-image-updater-config optional: true + - name: GIT_COMMIT_SIGNING_KEY + valueFrom: + configMapKeyRef: + key: git.commit-signing-key + name: argocd-image-updater-config + optional: true + - name: GIT_COMMIT_SIGNING_METHOD + valueFrom: + configMapKeyRef: + key: git.commit-signing-method + name: argocd-image-updater-config + optional: true + - name: GIT_COMMIT_SIGN_OFF + valueFrom: + configMapKeyRef: + key: git.commit-sign-off + name: argocd-image-updater-config + optional: true - name: IMAGE_UPDATER_KUBE_EVENTS valueFrom: configMapKeyRef: @@ -199,6 +217,10 @@ spec: name: ssh-config - mountPath: /tmp name: tmp + - mountPath: /app/.ssh/id_rsa + name: ssh-signing-key + readOnly: true + subPath: sshPrivateKey serviceAccountName: argocd-image-updater volumes: - configMap: @@ -218,5 +240,9 @@ spec: name: argocd-image-updater-ssh-config optional: true name: ssh-config + - name: ssh-signing-key + optional: true + secret: + secretName: ssh-git-creds - emptyDir: {} name: tmp diff --git a/pkg/argocd/git.go b/pkg/argocd/git.go index 71a610a5..1764a454 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -234,6 +234,13 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis defer os.Remove(cm.Name()) } + if wbc.GitCommitSigningKey != "" { + commitOpts.SigningKey = wbc.GitCommitSigningKey + } + + commitOpts.SigningMethod = wbc.GitCommitSigningMethod + commitOpts.SignOff = wbc.GitCommitSignOff + err = gitC.Commit("", commitOpts) if err != nil { return err diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index 98592d8b..3f6f2530 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -36,17 +36,20 @@ type ImageUpdaterResult struct { } type UpdateConfiguration struct { - NewRegFN registry.NewRegistryClient - ArgoClient ArgoCD - KubeClient *kube.KubernetesClient - UpdateApp *ApplicationImages - DryRun bool - GitCommitUser string - GitCommitEmail string - GitCommitMessage *template.Template - DisableKubeEvents bool - IgnorePlatforms bool - GitCreds git.CredsStore + NewRegFN registry.NewRegistryClient + ArgoClient ArgoCD + KubeClient *kube.KubernetesClient + UpdateApp *ApplicationImages + DryRun bool + GitCommitUser string + GitCommitEmail string + GitCommitMessage *template.Template + GitCommitSigningKey string + GitCommitSigningMethod string + GitCommitSignOff bool + DisableKubeEvents bool + IgnorePlatforms bool + GitCreds git.CredsStore } type GitCredsSource func(app *v1alpha1.Application) (git.Creds, error) @@ -63,17 +66,20 @@ type WriteBackConfig struct { Method WriteBackMethod ArgoClient ArgoCD // If GitClient is not nil, the client will be used for updates. Otherwise, a new client will be created. - GitClient git.Client - GetCreds GitCredsSource - GitBranch string - GitWriteBranch string - GitCommitUser string - GitCommitEmail string - GitCommitMessage string - KustomizeBase string - Target string - GitRepo string - GitCreds git.CredsStore + GitClient git.Client + GetCreds GitCredsSource + GitBranch string + GitWriteBranch string + GitCommitUser string + GitCommitEmail string + GitCommitMessage string + GitCommitSigningKey string + GitCommitSigningMethod string + GitCommitSignOff bool + KustomizeBase string + Target string + GitRepo string + GitCreds git.CredsStore } // The following are helper structs to only marshal the fields we require @@ -330,6 +336,11 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat if len(changeList) > 0 && updateConf.GitCommitMessage != nil { wbc.GitCommitMessage = TemplateCommitMessage(updateConf.GitCommitMessage, updateConf.UpdateApp.Application.Name, changeList) } + if updateConf.GitCommitSigningKey != "" { + wbc.GitCommitSigningKey = updateConf.GitCommitSigningKey + } + wbc.GitCommitSigningMethod = updateConf.GitCommitSigningMethod + wbc.GitCommitSignOff = updateConf.GitCommitSignOff } if needUpdate {