Kubefeeds Team A dedicated and highly skilled team at Kubefeeds, driven by a passion for Kubernetes and Cloud-Native technologies, delivering innovative solutions with expertise and enthusiasm.

Kubernetes GitOps with FluxCD – Part 2 – Secret Management using SOPS

4 min read

In Kubernetes-based GitOps workflows, securely managing sensitive information presents a unique challenge. While GitOps principles encourage storing all cluster configurations in Git repositories, committing plaintext secrets creates significant security risks. SOPS (Secrets OPerationS) offers an elegant solution to this problem when integrated with FluxCD.

In our previous post, we explored how to perform the initial setup of FluxCD in a Kubernetes cluster. Building on that foundation, we’ll now address one of the most critical aspects of GitOps implementation: Secret Management.

SOPS, developed by Mozilla, enables encrypting specific values within YAML, JSON, and other configuration formats while keeping the file structure intact. When combined with FluxCD’s native support for decryption, this allows teams to safely store encrypted secrets directly in their Git repositories alongside other infrastructure definitions. The secrets are only decrypted at runtime within the Kubernetes cluster, maintaining security while preserving the GitOps workflow.

This article explores how to implement a robust secret management strategy using SOPS and FluxCD. We’ll cover the setup process, encryption workflows, integration with different key management systems, and best practices for maintaining secure GitOps operations in production environments.

1. Install SOPS and GPG

Your installation method may differ depending on your operating system. Below is the command for openSUSE Tumbleweed:

sudo zypper in sops gpg2

For other distributions:

  • Ubuntu/Debian: sudo apt install sops gnupg2
  • macOS: brew install sops gnupg
  • Arch Linux: sudo pacman -S sops gnupg

2. Generate Keypair

Next, we’ll generate a GPG keypair that will be used for encryption and decryption:

export KEY_NAME="cluster.local"
export KEY_COMMENT="For Flux Secrets"

gpg --batch --full-generate-key <<EOF
%no-protection
Key-Type: 1
Key-Length: 4096
Subkey-Type: 1
Subkey-Length: 4096
Expire-Date: 0
Name-Comment: ${KEY_COMMENT}
Name-Real: ${KEY_NAME}
EOF

This creates a 4096-bit RSA key with no expiration date. The output will look similar to:

gpg: directory '/home/apurv/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/apurv/.gnupg/openpgp-revocs.d/A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4.rev'

Now retrieve the key fingerprint, which we’ll need for subsequent steps:

gpg --list-secret-keys "${KEY_NAME}"

gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
sec   rsa4096 2025-02-25 [SCEAR]
      A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4
uid           [ultimate] cluster.local (For Flux Secrets)
ssb   rsa4096 2025-02-25 [SEA]
      8B514FAAC98DF16F61EE487A45E294E6CACB03D4

Take note of the fingerprint (in this example: A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4).

3. Store Private Key as Kubernetes Secret

Now we’ll export the private key and store it as a Kubernetes secret that FluxCD can access:

gpg --export-secret-keys --armor "A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4" | kubectl create secret generic sops-gpg 
--namespace=flux-system 
--from-file=sops.asc=/dev/stdin

secret/sops-gpg created

Let’s verify the secret was created properly:

apurv@oxygen:~> kubectl -n flux-system get secrets

NAME          TYPE     DATA   AGE
flux-system   Opaque   3      25h
sops-gpg      Opaque   1      38s

Since the local key is unprotected (we used %no-protection for demonstration purposes), we should delete it from our local machine:

gpg --delete-secret-keys "A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4"

4. Configure In-Cluster Secrets Decryption

For production environments, the recommended approach is to store secrets in a separate repository with restricted access. However, for this tutorial, we’ll patch the existing cluster manifest to add support for the decryption provider.

Edit cluster/default/flux-system/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
 - gotk-components.yaml
 - gotk-sync.yaml
+patches:
+  - patch: |-
+      apiVersion: kustomize.toolkit.fluxcd.io/v1
+      kind: Kustomization
+      metadata:
+        name: flux-system
+        namespace: flux-system
+      spec:
+        decryption:
+          provider: sops
+          secretRef:
+            name: sops-gpg

This patch configures FluxCD to use SOPS for decryption, referencing our previously created GPG key secret.
Let’s commit and push our changes, then verify the deployment:

git commit -m "Configured SOPS decryption for main cluster repo" && git push origin

flux get kustomizations flux-system --watch

NAME            REVISION                SUSPENDED       READY   MESSAGE                              
flux-system     main@sha1:c35e4e45      False           True    Applied revision: main@sha1:c35e4e45

5. Export Public Key and Configure SOPS

We’ll export the public key to the cluster directory:

gpg --export --armor "A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4" > ./cluster/default/.sops.pub.asc

Next, create a SOPS configuration file to specify which parts of the YAML files should be encrypted:

cat <<EOF > ./cluster/default/.sops.yaml
creation_rules:
  - path_regex: .*.yaml
    encrypted_regex: ^(data|stringData)$
    pgp: A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4
EOF

This configuration tells SOPS to only encrypt the data and stringData sections of YAML files, leaving metadata intact for Kubernetes to process.
Commit these configuration files:

git add . && git commit -m "Added SOPS public key and config"

6. Validate the Setup

Let’s verify our setup by creating a sample secret and ensuring it works properly:

Create ./cluster/default/samplesecret.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: samplesecret
  namespace: default
type: Opaque
stringData:
  message: This is a secret message

Now encrypt this file with SOPS:

sops --encrypt --in-place samplesecret.yaml

After encryption the yaml file looks like below.

apiVersion: v1
kind: Secret
metadata:
    name: samplesecret
    namespace: default
type: Opaque
stringData:
    message: ENC[AES256_GCM,data:H0iMCWDGpqRSQZtjSXTbMlXaRvXQRxqs,iv:/r5ylcoqWd1wnOp1p9ksDUEs+kkPkdQz31I66LXrpxo=,tag:r2m9+dyo9tnBz+ty42cfrA==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age: []
    lastmodified: "2025-02-25T12:00:11Z"
    mac: ENC[AES256_GCM,data:AuJ9hHq2+Yars67Rw8QhkpjCu8iwqGVsWDJ2/1oEx430yaGSCNXXcZnntek9rsI3xuEzyVwTJcXIE/1yW1bCvPPdWUOel+dY+mLkLSUxio238FbDoGI8mv+7FLpDe4Tn9GZdBbaMyGF6FwXXfQJ8021tLSqK6IJQN8nW2XbT0L0=,iv:KkmRa+HzZ0lI0vsEdqX+ZY6HIpudA/0ABny6OruwuQ8=,tag:6gaSJssqOEQk53DvXCZT0Q==,type:str]
    pgp:
        - created_at: "2025-02-25T12:00:11Z"
          enc: |-
            -----BEGIN PGP MESSAGE-----

            hQIMA0XilObKywPUARAArblXLVn7VaJ+Sw2jNqptBPoPaDq7CA4V2LqUoGQUdIiX
            6pHZoZmfWCpvXAUTqLQ1xEnZ32XHl+lwEVxLzV6OckkYcZKxMGz1lHZGmK7QghtV
            Gm32lFvXVOnMfI9uZ96WH9WW+8Dng3VgyBAK/putNG+N3NLkeXa3vrWaaNaHY/Dj
            aBPK1FqzFAYLbFlGjadA1+1xpFfA4JnE5dLX5HTDIo9DKldJWxxqY6cGo6jTg1lO
            j+vmLHQicdKrApbMqdq1KyRLWTU21B0cRlfIIK37rX9xESXjUlwPmF27sew2KPsx
            iuLyofDu/fVD9kFUC/Zkrmog0IgBWaKuUniOBT1KkcPJoJ5AX9m3qyTGjTGNwvVU
            DWATppNCqnO2NhN2D13sOxL5/BSkJT4HZgdj8oIiQQjv0QwLW6nwlEHcmveiX6aK
            CRUOR51gp+ja+fAlZnevYUpJMJZFz4TX1LTVXqUd8dJeXePaAsCPzO38Ywrpe83D
            Z9n9ABYawCohwSb+Nd1/eoU0RBZwRfcD0PumTbWj8mbSMn6cPHJuAY1j1WZKH1/y
            PM583Vv1zA4NlUJwhAiqI/X0kl22+Qh4tq+tdrhPVLw7P+m7diHG2pyd6jKDZ/K9
            D3tY+eeQWcM3EV0hRBN7yA87Rs07lCy0m72PqAtD07qoUbO6jLXipIuE7TgPuZ3U
            aAEJAhBexKkWWprEJjk+jpt4h8aXyG7wUAeafCWr2kIWz0/kOSnG0STCjuL1kDbu
            +Ysah6EqMijU1sQBJv9Jn5oQ9eTAiHwN2Brh8F1nCPT+E6Ih6lbiJSLAD8duEa7V
            Nsgo8cYB0ebx
            =R3G0
            -----END PGP MESSAGE-----
          fp: A2E9A640A0E47EB72E6A4F0517B6FD7A7486D6E4
    encrypted_regex: ^(data|stringData)$
    version: 3.9.4

Lets push these changes.

git add samplesecret.yaml && git commit -m "Added sample secret"
git push origin

Let’s verify that the secret is created in the cluster:

kubectl get secrets
NAME           TYPE     DATA   AGE
samplesecret   Opaque   1      52s

Verify the content of the secret:

kubectl get secret samplesecret -o jsonpath='{.data.message}' | base64 --decode

This is a secret message

What next ?

Future posts will explore advanced GitOps patterns with FluxCD, including:

  • Helm chart automation
  • Image update automation
  • Notification and alerting configuration

Stay tuned for each of these topics.

References

Kubefeeds Team A dedicated and highly skilled team at Kubefeeds, driven by a passion for Kubernetes and Cloud-Native technologies, delivering innovative solutions with expertise and enthusiasm.