From 759efce84dad37342789101bb2575ab9b3e54b5e Mon Sep 17 00:00:00 2001 From: Dustin Lactin Date: Sat, 11 May 2024 21:14:20 -0600 Subject: [PATCH] Added support for ssh signed commits and completed gpg signed commit work Signed-off-by: Dustin Lactin --- .github/actions/spelling/expect.txt | 5 ++ cmd/main.go | 2 + cmd/run.go | 22 +++++---- docs/basics/update-methods.md | 49 ++++++++++++++++++- ext/git/client.go | 1 + ext/git/mocks/Client.go | 14 ++++++ ext/git/writer.go | 39 ++++++++++++++- .../argocd-image-updater-deployment.yaml | 24 +++++++++ manifests/install.yaml | 24 +++++++++ pkg/argocd/git.go | 14 ++++++ pkg/argocd/update.go | 48 ++++++++++-------- 11 files changed, 210 insertions(+), 32 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 642a1bc4..d0867389 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,7 +14,9 @@ eec fbd ffb gitlab +GPG helmvalues +html installationid jfrog mep diff --git a/cmd/main.go b/cmd/main.go index 3a4019dd..ed003f79 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -44,6 +44,8 @@ type ImageUpdaterConfig struct { GitCommitUser string GitCommitMail string GitCommitMessage *template.Template + GitCommitSigningKey string + GitCommitSignOff bool DisableKubeEvents bool } diff --git a/cmd/run.go b/cmd/run.go index 305863db..27c79c31 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -221,6 +221,8 @@ 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 Public SSH Key used to sign the commits") + 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") @@ -300,15 +302,17 @@ 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, + 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, + GitCommitSignOff: cfg.GitCommitSignOff, + DisableKubeEvents: cfg.DisableKubeEvents, } res := argocd.UpdateApplication(upconf, syncState) result.NumApplicationsProcessed += 1 diff --git a/docs/basics/update-methods.md b/docs/basics/update-methods.md index 404b5aa4..1f3f0a76 100644 --- a/docs/basics/update-methods.md +++ b/docs/basics/update-methods.md @@ -153,7 +153,8 @@ format. To create such a secret from an existing private key, you can use ```bash kubectl -n argocd-image-updater create secret generic git-creds \ - --from-file=sshPrivateKey=~/.ssh/id_rsa + --from-file=sshPrivateKey=~/.ssh/id_rsa \ + --from-file=sshPublicKey=~/.ssh/id_rsa.pub \ ``` ### Specifying a repository when using a Helm repository in repoURL @@ -247,6 +248,52 @@ 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 verification using an SSH or GPG key +Commit signing requires 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 public 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 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) + +Commit Sign Off can be enabled by setting `git.commit-sign-off: "true"` + +**SSH:** + +Both private and public keys 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 public key: + +```yaml +data: + git.commit-sign-off: "true" + git.commit-signing-key: /app/.ssh/id_rsa.pub +``` + +The matching private key must be available in the same location. + +Create a new SSH secret or add the public key to your existing SSH secret: +```bash +kubectl -n argocd-image-updater create secret generic ssh-git-creds \ + --from-file=sshPrivateKey=~/.ssh/id_rsa \ + --from-file=sshPublicKey=~/.ssh/id_rsa.pub +``` + +**GPG:** + +The GPG private key must be installed and available in the `argocd-image-updater` pod. +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 +``` + ### 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/client.go b/ext/git/client.go index ac9dc105..5fed9d3d 100644 --- a/ext/git/client.go +++ b/ext/git/client.go @@ -82,6 +82,7 @@ type Client interface { Add(path string) error SymRefToBranch(symRef string) (string, error) Config(username string, email string) error + SigningConfig(signingkey string) error } type EventHandlers struct { 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 20a678f0..2368f8ea 100644 --- a/ext/git/writer.go +++ b/ext/git/writer.go @@ -2,6 +2,7 @@ package git import ( "fmt" + "regexp" "strings" "github.com/argoproj-labs/argocd-image-updater/pkg/log" @@ -13,7 +14,7 @@ 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 Public SSH Key used to sign the commit with (-S option) SigningKey string // SignOff specifies whether to sign-off a commit (-s option) SignOff bool @@ -32,7 +33,16 @@ func (m *nativeGitClient) Commit(pathSpec string, opts *CommitOptions) error { args = append(args, "-a") } if opts.SigningKey != "" { - args = append(args, "-S", opts.SigningKey) + // Check if SiginingKey is a GPG key or Public SSH Key + keyCheck, err := regexp.MatchString(".*pub$", opts.SigningKey) + if err != nil { + return fmt.Errorf("could not validate Signing Key as GPG or Public SSH Key: %v", err) + } + if keyCheck { + args = append(args, "-S") + } else { + args = append(args, "-S", opts.SigningKey) + } } if opts.SignOff { args = append(args, "-s") @@ -116,3 +126,28 @@ func (m *nativeGitClient) Config(username string, email string) error { return nil } + +// SigningConfig configures commit signing for the repository +func (m *nativeGitClient) SigningConfig(signingkey string) error { + // Check if SiginingKey is a GPG key or Public SSH Key + keyCheck, err := regexp.MatchString(".*pub$", signingkey) + if err != nil { + return fmt.Errorf("could not validate Signing Key as GPG or Public SSH Key: %v", err) + } + if keyCheck { + // Setting the GPG format to ssh + log.Warnf("Setting GPG Format to SSH") + _, err = m.runCmd("config", "gpg.format", "ssh") + if err != nil { + return fmt.Errorf("could not set gpg format to ssh: %v", err) + } + // Setting Public SSH Key as our signing key + // SSH Keys can not currently be set via cli flag + _, err = m.runCmd("config", "user.signingkey", signingkey) + if err != nil { + return fmt.Errorf("could not set git signing key: %v", err) + } + } + + return nil +} diff --git a/manifests/base/deployment/argocd-image-updater-deployment.yaml b/manifests/base/deployment/argocd-image-updater-deployment.yaml index 5682ed78..4febed29 100644 --- a/manifests/base/deployment/argocd-image-updater-deployment.yaml +++ b/manifests/base/deployment/argocd-image-updater-deployment.yaml @@ -77,6 +77,18 @@ 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_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 +128,14 @@ spec: name: ssh-config - mountPath: /tmp name: tmp + - name: ssh-signing-key + mountPath: /app/.ssh/id_rsa + readOnly: true + subPath: sshPrivateKey + - name: ssh-signing-key + mountPath: /app/.ssh/id_rsa.pub + readOnly: true + subPath: sshPublicKey serviceAccountName: argocd-image-updater volumes: - configMap: @@ -135,5 +155,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..47b9e01f 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -158,6 +158,18 @@ 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_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 +211,14 @@ spec: name: ssh-config - mountPath: /tmp name: tmp + - mountPath: /app/.ssh/id_rsa + name: ssh-signing-key + readOnly: true + subPath: sshPrivateKey + - mountPath: /app/.ssh/id_rsa.pub + name: ssh-signing-key + readOnly: true + subPath: sshPublicKey serviceAccountName: argocd-image-updater volumes: - configMap: @@ -218,5 +238,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..4ced14e6 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -169,6 +169,14 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis } } + // Set commit signing configuration + if wbc.GitCommitSigningKey != "" { + err = gitC.SigningConfig(wbc.GitCommitSigningKey) + if err != nil { + return err + } + } + // The branch to checkout is either a configured branch in the write-back // config, or taken from the application spec's targetRevision. If the // target revision is set to the special value HEAD, or is the empty @@ -234,6 +242,12 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis defer os.Remove(cm.Name()) } + if wbc.GitCommitSigningKey != "" { + commitOpts.SigningKey = wbc.GitCommitSigningKey + } + + 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 0f1cab28..a35d0616 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -36,16 +36,18 @@ 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 + NewRegFN registry.NewRegistryClient + ArgoClient ArgoCD + KubeClient *kube.KubernetesClient + UpdateApp *ApplicationImages + DryRun bool + GitCommitUser string + GitCommitEmail string + GitCommitMessage *template.Template + GitCommitSigningKey string + GitCommitSignOff bool + DisableKubeEvents bool + IgnorePlatforms bool } type GitCredsSource func(app *v1alpha1.Application) (git.Creds, error) @@ -62,16 +64,18 @@ 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 + GitClient git.Client + GetCreds GitCredsSource + GitBranch string + GitWriteBranch string + GitCommitUser string + GitCommitEmail string + GitCommitMessage string + GitCommitSigningKey string + GitCommitSignOff bool + KustomizeBase string + Target string + GitRepo string } // The following are helper structs to only marshal the fields we require @@ -323,6 +327,10 @@ 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.GitCommitSignOff = updateConf.GitCommitSignOff } if needUpdate {