Setting up access to a private repository in ArgoCD with SSM Parameter Store and External Secrets Operator Nov 26 2022
ArgoCD's documentation is quite good. I just feel there is one key question that is often left unanswered. How do I get my private SSH key into ArgoCD in a declarative way that doesn't require hard coding the key into a secret YAML file?
In this post, we are going to use the External Secrets Operator (ESO) to get the private SSH key from AWS SSM Parameter Store and inject it into ArgoCD using a Kubernetes Secret.
If you already have ArgoCD setup, skip directly to the section Setting up ArgoCD for private repositories. If not, follow along.
Ok, let's get started.
Bash Beyond Basics Increase your efficiency and understanding of the shell
If you are interested in this topic you might enjoy my course Bash Byond Basics. This course helps you level up your bash skills. This is not a course on shell-scripting, is a course on improving your efficiency by showing you the features of bash that are seldom discussed and often ignored.
Every day you spend many hours working in the shell, every little improvement in your worklflows will pay dividends many fold!
Learn moreInstalling ArgoCD
First, we need to install ArgoCD in our Kubernetes cluster. We will use Helm to do this, but we will do it in a declarative way so we can use our code as the source of truth. Let's start by creating the directory and the required files for our Helm chart.
1
2
mkdir argo-cd
touch argo-cd/{Chart,requirements}.yaml
The content for the Chart.yaml
file is:
1
2
name: argo-cd
version: 0.1.0
The content for requirements.yaml
is:
1
2
3
4
dependencies:
- name: argo-cd
version: 5.13.6
repository: https://argoproj.github.io/argo-helm
Now we can install the chart:
1
2
3
cd argo-cd/
helm dependency update
helm install argo-cd . -n argocd --create-namespace
That should get ArgoCD set up in our Kubernetes Cluster. We need a repository with some Kubernetes templates. If you don't have one, you can use the one I created for this post. It is a public repository. You can find it here. Fork it, and work with it. Later you can make it private or create a different private repository.
Create the ArgoCD Project and application
We will test that everything works correctly with ArgoCD using a public repository. Once ArgoCD runs correctly, we can focus on using a private repo. You can skip this section if you don't want to run these tests. Let's now create a new ArgoCD project and application.
The project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: server-proj
namespace: argocd
# Finaliser that ensures that the project is not deleted until any application does not reference it
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
# Project description
description: Server Project
destinations:
# Update your namespace
- namespace: my-namespace
server: https://kubernetes.default.svc
sourceRepos:
# Make sure to add your repository here
- https://github.com/rderik/slartybartfast.git
clusterResourceWhitelist:
- group: ''
kind: '*'
The application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: server
namespace: argocd
spec:
destination:
namespace: my-namespace
server: https://kubernetes.default.svc
project: server-proj
source:
# Change the repository to your repository
repoURL: https://github.com/rderik/slartybartfast.git
# point the path to the path where your Kubernetes templates are
path: server
syncPolicy:
automated: # automated sync by default retries failed attempts 5 times with following delays between attempts ( 5s, 10s, 20s, 40s, 80s ); retry controlled using `retry` field.
prune: false # Specifies if resources should be pruned during auto-syncing ( false by default ).
selfHeal: false # Specifies if partial app sync should be executed when resources are changed only in target Kubernetes cluster and no git change detected ( false by default ).
allowEmpty: true # Allows deleting all application resources during automatic syncing ( false by default ).
Ok, we can create the project and the application:
1
2
kubectl apply -f project.yaml
kubectl apply -f application.yaml
Finally, we can view our application in the ArgoCD dashboard by doing a port forwarding to the argocd-server service:
1
kubectl port-forward svc/argocd-server -n argocd 8080:443
I run most of my tests in a VM, so when I do port forwarding, I need the port to be bound to all interfaces:
1
kubectl port-forward svc/argo-cd-argocd-server -n argocd 8080:443 --address=0.0.0.0
We can now visit the ArgoCD dashboard at https://localhost:8080 and log in with the default username and password. The default username is admin
. To obtain the default password, you need to read a secret created by default during installation.
1
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64 -d
Log in, and you should see your application being created!
If we do a port forward to the newly created pod, we can see the Nginx welcome page:
1
kubectl port-forward $(kubectl get pods -n my-namespace --no-headers | awk '{print $1}') 8081:80 -n my-namespace
In another terminal:
1
curl localhost:8081
Ok, now we have a working ArgoCD installation. We can focus on setting up the private repository access.
Setting up the private repository access
We need to create an SSH key pair. We will use the key pair to access the private repository.
1
$ ssh-keygen -t rsa -b 4096 -C "key for private repository" -f id_rsa_private_repo
Now we need to add the public key to the repository. In my case, I'm using GitHub, so I need to add the public key to the repository. If you also use GitHub, go to the repository Settings > Deploy Keys, and add the PUBLIC key. We only need read-only, so there is no need to add write permissions.
Let's create a secure string parameter in AWS SSM parameter store that contains the PEM-encoded private key. We will use this as a secret to add the private key to ArgoCD.
1
2
3
4
aws ssm put-parameter \
--name "/argo-cd/github-repo/ssh-key" \
--type SecureString \
--value "$(cat id_rsa_private_repo)"
Finally, now we can look at setting up ArgoCD for our private repository.
Setting up ArgoCD for private repositories
We need to create a secret that will contain the private key. We will use the private key to access the private repository.
ArgoCD's documentation explains that we need to create a secret with the private key so ArgoCD can access the repository. The secret looks like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Secret
metadata:
name: private-repo
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: git@github.com:argoproj/my-private-repository
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
This approach has the issue that we would have to write our private key in plain text in the secret template. We don't want to do that. So we have a few options to avoid that. We can create a template and documentation that explains how to fill up this file and then manually apply it. That works, but it is a little bit cumbersome. We'll use External Secrets Operator and avoid doing that.
Setting up External Secrets Operator
As we did before for the other dependencies, we will set up the External Secrets Operator in a declarative way using Helm.
1
2
mkdir external-secrets-operator/
touch external-secrets-operator/{Chart,requirements}.yaml
The content for the Chart.yaml
file is:
1
2
name: external-secrets-operator
version: 0.1.0
The content for requirements.yaml
is:
1
2
3
4
dependencies:
- name: external-secrets
version: 0.6.1
repository: https://charts.external-secrets.io
Now we can install the chart:
1
2
3
cd external-secrets-operator
helm dependency update
helm install external-secrets-operator . -n external-secrets-operator --create-namespace
Perfect, we have everything set up. Now we can create the secret that will contain the private key.
Creating the secret
Make sure that your IAM role has access to the SSM parameter store. The role will need a policy similar to the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
resource "aws_iam_policy" "ssm_parameter_policy" {
name = "cluster-ssm-parameter-policy"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : [
"ssm:DescribeParameters",
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Effect" : "Allow",
"Resource" : [
"arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:/argo-cd/*"
]
},
]
})
}
With that out of the way, let's define the Secret Store:
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: my-aws-secret-store
# we need the secret to exist in the same namespace as ArgoCD
namespace: argocd
spec:
provider:
aws:
service: ParameterStore
# define a specific role to limit access
# to certain secrets
region: us-east-1
We are going to get the AWS credentials from the current environment. If you wish to use a secret and key stored in a Kubernetes secret, you could add the section:
1
2
3
4
5
6
7
8
9
10
# Auth defines the information necessary to authenticate against AWS by
# getting the accessKeyID and secretAccessKey from an already created Kubernetes Secret
auth:
secretRef:
accessKeyID:
name: awssm-secret
key: access-key
secretAccessKey:
name: awssm-secret
key: secret-access-key
Reference: https://external-secrets.io/v0.6.1/api/secretstore/
Ok, now we can create the external secret. If we take a look at the documentation, the secret should look like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Secret
metadata:
name: private-repo
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: git@github.com:argoproj/my-private-repository
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
To generate a secret with that structure, we will use the template
feature of the External Secrets Operator to create the secret. The external secret will look like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: private-repo-ssh-key
namespace: argocd
spec:
# SecretStoreRef defines which SecretStore to use when fetching the secret data
secretStoreRef:
name: my-aws-secret-store
kind: SecretStore # or ClusterSecretStore
# Specify a blueprint for the resulting Kind=Secret
target:
name: my-aws-secret-repo-ssh-key
template:
metadata:
labels:
argocd.argoproj.io/secret-type: repository
# Use inline templates to construct your desired config file that contains your secret
data:
type: git
url: git@github.com:rderik/slartypriv.git
sshPrivateKey: |
{{ .sshPrivateKey | toString }}
data:
- secretKey: sshPrivateKey
remoteRef:
key: /argo-cd/github-repo/ssh-key
Remember to update the url
and your SSM parameter key to match your repositories. Remember you are going to be using ssh and not https
.
With that out of the way, we can apply the two templates:
1
2
kubectl apply -f secret-store.yaml
kubectl apply -f external-secret.yaml
If we get any errors and need to review what is going on, we can use the following command:
1
2
kubectl describe SecretStore my-aws-secret-store -n argocd
kubectl describe ExternalSecret private-repo-ssh-key -n argocd
And if we want to check if the secret created in Kubernetes matches our SSH key:
1
2
kubectl get secret my-aws-secret-repo-ssh-key \
-n argocd -o jsonpath="{.data.sshPrivateKey}" | base64 -d
That's it. Now we can use our private repository in ArgoCD.
I created a private repository with the following url
:
1
git@github.com:rderik/slartypriv.git
And the project in ArgoCD looks like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: server-proj-private
namespace: argocd
# Finaliser that ensures that the project is not deleted until any application does not reference it
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
# Project description
description: Server Project
destinations:
# Update your namespace
- namespace: my-namespace-private
server: https://kubernetes.default.svc
sourceRepos:
# Make sure to add your repository here
- git@github.com:rderik/slartypriv.git
clusterResourceWhitelist:
- group: ''
kind: '*'
The application looks like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: server-private
namespace: argocd
spec:
destination:
namespace: my-namespace-private
server: https://kubernetes.default.svc
project: server-proj-private
source:
# Change the repository to your repository
repoURL: git@github.com:rderik/slartypriv.git
# point the path to the path where your Kubernetes templates are
path: server
syncPolicy:
automated:
prune: false
selfHeal: false
allowEmpty: true
Notice that the sourceRepos
and the repoURL
have changed from https
to ssh, so update your repository URLs correctly to use ssh. If we apply the project and the application, we should see it working without a problem in the ArgoCD dashboard.
1
2
kubectl apply -f project-private.yaml
kubectl apply -f application-private.yaml
And That's it! We now have a private repository in ArgoCD.
Final thoughts
There are many moving parts when we are working with Kubernetes and ArgoCD, and we need to pay a lot of attention to all the small details. I got stuck in the past debugging for a long time until I realised that the repoURL
was still using https
instead of ssh. And I got stuck another time until I realised I had a typo on the Secret template.
Anyways, I hope this is useful.
References
- External Secrets Operator (ESO)
- ESO - AWS Parameter Store Provider
- ESO - ExternalSecret
- ESO - SecretStore
- ESO - advanced templating
- ArgoCD - declarative setup