Skip to content

Commit

Permalink
Merge pull request #44 from utilitywarehouse/as-golden-ssh-key
Browse files Browse the repository at this point in the history
added global git ssh key support
  • Loading branch information
asiyani authored Aug 18, 2023
2 parents 55bc8cc + 2f4e665 commit b0bd4b3
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 197 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.20-alpine AS build
FROM golang:1.21-alpine AS build

ENV \
STRONGBOX_VERSION=1.1.0 \
Expand Down
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
An Argo CD plugin to decrypt strongbox encrypted files and build Kubernetes resources.
plugin supports argocd version from 2.4 onwards and only same cluster deployments are supported.

This plugin has 2 commands
This plugin has 1 command

### `decrypt`
command will read kube secret containing keyring data and run strongbox decryption using this data.
### `generate`
generate command does following 2 things

1) it will read kube secret containing keyring data and run strongbox decryption using this data.
if multiple keys are used to encrypt app secrets then this secret should contain all the keys.

### `generate`
command will run kustomize build to generate kube resources's yaml strings. it will print this yaml stream to stdout.
2) command will run kustomize build to generate kube resources's yaml strings. it will print this yaml stream to stdout.

#### private repository

To fetch remote base from private repository, admin can add global ssh key which will be used for ALL applications.

Plugin support fetching remote base from private repositories, to do that user must create a secret with name `argocd-voodoobox-git-ssh`,

user can also provide own ssh keys for an applications via 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: <key_file_name>` comment above it.
if only 1 ssh key is used for ALL private repos then there is no need to specify this comment.
Expand Down Expand Up @@ -128,22 +134,19 @@ data:
allowConcurrency: true
discover:
fileName: "*"
init:
command:
- 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:
- "--global-git-ssh-key-file=/path/to/global/key"
- "--global-git-ssh-known-hosts-file=/path/to/global/khf"
- "--app-strongbox-secret-name=argocd-voodoobox-strongbox-keyring"
- "--app-git-ssh-secret-name=argocd-voodoobox-git-ssh"
- "--secret-allowed-namespaces-annotation=argocd.voodoobox.plugin.io/allowed-namespaces"
- "--allowed-namespaces-secret-annotation=argocd.voodoobox.plugin.io/allowed-namespaces"
lockRepo: false
```
* Instead of setting up arguments via configMap we can also set corresponding ENVs on plugin side car

### 2. patch `argocd-repo-server` deployment to add sidecar as shown
volume from `cmp-plugin` configMap and mount it to `/home/argocd/cmp-server/config/plugin.yaml`
Expand Down Expand Up @@ -228,7 +231,9 @@ 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 |
| --allowed-namespaces-secret-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 |
| --global-git-ssh-key-file | | The path to git ssh key file which will be used as global ssh key to fetch kustomize base from private repo for all application |
| --global-git-ssh-known-hosts-file | | The path to git known hosts file which will be used as with global ssh key to fetch kustomize base from private repo for all application |
| --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 |
Expand Down
2 changes: 1 addition & 1 deletion decrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func Test_getKeyRingData(t *testing.T) {
}

func Test_ensureDecryption(t *testing.T) {
secretAllowedNamespacesAnnotation = "argocd.voodoobox.plugin.io/allowed-namespaces"
allowedNamespacesSecretAnnotation = "argocd.voodoobox.plugin.io/allowed-namespaces"

// read keyring file
kr, err := os.ReadFile(encryptedTestDir1 + "/.keyRing")
Expand Down
4 changes: 2 additions & 2 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"strings"
)

func ensureBuild(ctx context.Context, cwd string, app applicationInfo) (string, error) {
func ensureBuild(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile 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
Expand All @@ -33,7 +33,7 @@ func ensureBuild(ctx context.Context, cwd string, app applicationInfo) (string,
}

if hasRemoteBase {
sshCmdEnv, err = setupGitSSH(ctx, cwd, app)
sshCmdEnv, err = setupGitSSH(ctx, cwd, globalKeyPath, globalKnownHostFile, app)
if err != nil {
return "", err
}
Expand Down
88 changes: 54 additions & 34 deletions git-ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
Expand Down Expand Up @@ -32,60 +33,74 @@ var (
reRepoURLWithSSH = regexp.MustCompile(`(?P<beginning>^\s*-\s*(?:ssh:\/\/)?)(?P<user>\w.+?@)?(?P<domain>\w.+?)(?P<repoDetails>[\/:].*$)`)
)

func setupGitSSH(ctx context.Context, cwd string, app applicationInfo) (string, error) {
func setupGitSSH(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile string, app applicationInfo) (string, error) {
knownHostsFragment := `-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`

sec, err := getSecret(ctx, app.destinationNamespace, app.gitSSHSecret)
if err != nil {
return "", fmt.Errorf("unable to get secret err:%v", err)
}

sshDir := filepath.Join(cwd, SSHDirName)
if err := os.Mkdir(sshDir, 0700); err != nil {
return "", fmt.Errorf("unable to create ssh config dir err:%s", err)
}
sshConfigFilename := filepath.Join(sshDir, "config")

// keyFilePaths holds key name and path values
var keyFilePaths = make(map[string]string)
// keyFilePaths holds key name and domain it should be used for as values
var keyedDomain = make(map[string]string)
var userKnownHostFile string

// attempt to get secret with user's own ssh keys if not found continue with just global key
sec, err := getSecret(ctx, app.destinationNamespace, app.gitSSHSecret)
if err != nil && !errors.Is(err, errNotFound) {
return "", err
}

// write ssh data to ssh dir
for k, v := range sec.Data {
if k == "known_hosts" {
if err := os.WriteFile(filepath.Join(sshDir, k), v, 0600); err != nil {
return "", fmt.Errorf("unable to write known_hosts to temp file err:%s", err)
if sec != nil {
// write ssh data to ssh dir
for k, v := range sec.Data {
if k == "known_hosts" {
if err := os.WriteFile(filepath.Join(sshDir, k), v, 0600); err != nil {
return "", fmt.Errorf("unable to write known_hosts to temp file err:%s", err)
}
userKnownHostFile = sshDir + `/known_hosts`
continue
}
// if key is not known_hosts then its assumed to be private keys
kfn := filepath.Join(sshDir, k)
// if the file containing the SSH key does not have a
// newline at the end, ssh does not complain about it but
// the key will not work properly
if !bytes.HasSuffix(v, []byte("\n")) {
v = append(v, byte('\n'))
}
keyFilePaths[k] = kfn
if err := os.WriteFile(kfn, v, 0600); err != nil {
return "", fmt.Errorf("unable to write key to temp file err:%s", err)
}
knownHostsFragment = fmt.Sprintf(`-o UserKnownHostsFile=%s/known_hosts`, sshDir)
continue
}
// if key is not known_hosts then its assumed to be private keys
kfn := filepath.Join(sshDir, k)
// if the file containing the SSH key does not have a
// newline at the end, ssh does not complain about it but
// the key will not work properly
if !bytes.HasSuffix(v, []byte("\n")) {
v = append(v, byte('\n'))
}
keyFilePaths[k] = kfn
if err := os.WriteFile(kfn, v, 0600); err != nil {
return "", fmt.Errorf("unable to write key to temp file err:%s", err)
}
}

keyedDomain, err := processKustomizeFiles(cwd)
if err != nil {
return "", fmt.Errorf("unable to updated kustomize files err:%s", err)
keyedDomain, err = processKustomizeFiles(cwd)
if err != nil {
return "", fmt.Errorf("unable to updated kustomize files err:%s", err)
}
}

sshConfigFilename := filepath.Join(sshDir, "config")

body, err := constructSSHConfig(keyFilePaths, keyedDomain)
body, err := constructSSHConfig(keyFilePaths, keyedDomain, globalKeyPath)
if err != nil {
return "", err
}
if err := os.WriteFile(sshConfigFilename, body, 0600); err != nil {
return "", err
}

switch {
case globalKnownHostFile != "" && userKnownHostFile != "":
knownHostsFragment = `-o UserKnownHostsFile=` + globalKnownHostFile + ` -o UserKnownHostsFile=` + userKnownHostFile
case globalKnownHostFile != "":
knownHostsFragment = `-o UserKnownHostsFile=` + globalKnownHostFile
case userKnownHostFile != "":
knownHostsFragment = `-o UserKnownHostsFile=` + userKnownHostFile
}

return fmt.Sprintf(`GIT_SSH_COMMAND=ssh -q -F %s %s`, sshConfigFilename, knownHostsFragment), nil
}

Expand Down Expand Up @@ -203,7 +218,7 @@ func replaceDomainWithConfigHostName(original string, keyName string) (string, s
return newURL, domain, nil
}

func constructSSHConfig(keyFilePaths map[string]string, keyedDomain map[string]string) ([]byte, error) {
func constructSSHConfig(keyFilePaths map[string]string, keyedDomain map[string]string, globalKeyPath string) ([]byte, error) {
hostFragments := []string{}
for keyName, domain := range keyedDomain {
keyFilePath, ok := keyFilePaths[keyName]
Expand All @@ -215,7 +230,12 @@ func constructSSHConfig(keyFilePaths map[string]string, keyedDomain map[string]s
hostFragments = append(hostFragments, fmt.Sprintf(hostFragment, host, domain, keyFilePath))
}

if len(keyFilePaths) == 1 {
if globalKeyPath != "" {
hostFragments = append(hostFragments, fmt.Sprintf(singleKeyHostFragment, globalKeyPath))
}

// if global key is not provided and user provides only 1 key then also use that for all host
if len(keyFilePaths) == 1 && globalKeyPath == "" {
for _, keyFilePath := range keyFilePaths {
hostFragments = append(hostFragments, fmt.Sprintf(singleKeyHostFragment, keyFilePath))
}
Expand Down
Loading

0 comments on commit b0bd4b3

Please sign in to comment.