Mozilla SOPS (Secrets OPerationS) is a simple and flexible tool for managing secrets. SOPS provides the scaffolding to enable the use of various encryption solutions to encrypt and decrypt select values in files whilst leaving the keys in plain-text.

SOPS supports yaml, json, env, ini and binary formats and can integrate with;

  • Amazon Web Services Key Managemnt Service
  • Google Cloud Platforms Key Management Service
  • Azure Key Vault
  • age
  • and Pretty Good Privacy (PGP)

When looking at options for secret management I was searching for a solution that would have minimal overhead and enable me to keep my secrets in git along with the code. With SOPS, I can use my existing hardware backed PGP key that I use to sign my git commits as well as a secondary PGP key stored in the git repository, the private half of which only exists in GitLab for CI/CD operations.

The following example shows the beauty of SOPS, the secret is encrypted, yet the file is otherwise readable.

$ sops encrypt --encrypted-regex '^(password)$' kubernetes-secret.yaml

apiVersion: v1
kind: Secret
metadata:
    name: database-password
type: Opaque
data:
    password: ENC[AES256_GCM,data:hbu4wKZJbY+7YcT85yUZq7KqXA==\
              ,iv:mXft34AvjoRmYlXbcZ3H7qNpwkGaXDKTQ/NUjk2t4Ao=,\
              tag:mqhdJX7rQJZWT664aUOd7w==,type:str]

SOPS encrypted files are self-contained and include all the information required to decrypt the file aslong as you possess the private key. SOPS appends a footer to the file that contains metadata and the master key(s) to decrypt the file. For brevity I have omitted the footer from the example above, however a complete example of a SOPS encrypted file is included in the example towards the end of this post.

Setting up SOPS with PGP

Generate PGP Keys

The following example will generate a PGP on-disk. As stated previously I use a hardware backed PGP key along with a secondary on-disk PGP key, the public half remains in the git repository with the private half only existing in GitLab for CI/CD operations.

  1. To generate a PGP key you can either use the snippet below or run gpg --full-generate-key to use the interactive wizard
export KEY_NAME="GitLab SOPS"
export KEY_COMMENT="Used for automated SOPS operations"

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
  1. With the PGP key created you an export the private key for use in GitLab and the public key for use in the git repository. Note that both the private and public key should be secured and backed up.
$ gpg -a --export > gitlab-sops-public.asc

$ gpg -a --export-secret-keys > gitlab-sops-private.asc

Install and Configure SOPS

  1. Install SOPS
sudo dnf install --assume-yes sops
  1. In order to configure SOPS we’ll need the PGP key fingerprints
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ gpg --list-key

/var/home/adam/.gnupg/onlykey/pubring.kbx
-----------------------------------------
pub   ed25519 2023-04-26 [SC]
      D926965E63B13548A8F05AC137F86304FE9CA39J
uid           [ultimate] Adam Gould <[email protected]>
sub   cv25519 2023-04-26 [E]

pub   rsa4096 2025-07-27 [SCEA]
      BCE1F3023A9ABFCC2561AB34CA7CB93D9744872D
uid           [ultimate] GitLab SOPS (Used for automated SOPS operations)
sub   rsa4096 2025-07-27 [SEA]
  1. Within your git repository create a file .sops.yaml. The creation rule is used by SOPS to selectively encrypt all files with the specified keys. The path can be altered to use different PGP keys for different paths if desired.
creation_rules:
  - path_regex: .*
    encrypted_regex: '^(password)$'
    key_groups:
    - pgp: 
        - D926965E63B13548A8F05AC137F86304FE9CA39J
        - BCE1F3023A9ABFCC2561AB34CA7CB93D9744872D
  1. With the .sops.yaml configured, we can now encrypt select keys as follows matching regular expression in the .sops.yaml file

Note that the footer includes the respective key fingerprints and master key to decrypt the file.

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ sops encrypt --in-place kubernetes-secret.yaml

apiVersion: v1
kind: Secret
metadata:
    name: database-password
type: Opaque
data:
    password: ENC[AES256_GCM,data:QhYfJjjaPUAXcizJdIccal871A==,\
              iv:1gvWd8N19PALxps7EtxOUTcicJGnw54V/fTPg0ALsmY=,\
              tag:Ld4J2E4XrLg8NDMVNaArKA==,type:str]
sops:
    lastmodified: "2025-07-27T11:28:25Z"
    mac: ENC[AES256_GCM,data:kPzh2EsUzkX5OF+AoHVeNUqnDEw2/4B+9\
         /QaHJAwynnjRzSz15xqqvFobnpTWwyy6SLRampxyJpmpYNgCYc13H\
         C25HziJ9mcTbx8nbBNtNrJ9XaH9Kon/MgtNenulISheIn4yrSpi782\
         XZ6AvMvtg14nxz3p9JjP6oRbGvWg81Q=,iv:wxvGM8HrjTgqQLqh8GV\
         xhbh3U+RK6I/Lr1bf8yoZjnI=,tag:VvG74E3p0MeYhdXEI3J4Pg==\
         ,type:str]
    pgp:
        - created_at: "2025-07-27T11:28:25Z"
          enc: |-
            -----BEGIN PGP MESSAGE-----

            hF4DvaP1AhNAQIISAQdADCYlBNbnfsw+izd5SmCxNUEtpszfMHxQ/RTkcxmxUD4w
            v0R31gr7D3GgQFQ5iBdjoW0sXyDgDxDQrrZOAGt4TUAm8ik9JsdZtd/ncE4uZ+2u
            0lwBv7FbNAeDVS2IbX4PDuAQVGp07HPmXjJYWn/GUHEq8J0US1V3R0xQrF+v9wMi
            iUXjLJaZBVsEK2LD406kRZrc1vXA567RbITjtV+fcUTuzRBGaX94d2WULcgu5w==
            =cON4
            -----END PGP MESSAGE-----
          fp: D926965E63B13548A8F05AC137F86304FE9CA39J
        - created_at: "2025-07-27T11:28:25Z"
          enc: |-
            -----BEGIN PGP MESSAGE-----

            hQIMA8mPQcgfHvAMAQ//cUp/JBq10aTU/CcYnMIXVIJlTh14g5ynK2qcSdYKxm1a
            v0Jzmx1iEVKTBBHf67sKlxMSufEp93D6pTcapeJzN7Y052M57ExiwlQyJZThYsRG
            hP7q+OKlJcwERT4vRK/z83cmVPmfDElD3m5IZlTR6ixxdnsTp+NoCvzy04wmluvS
            UolcnWLDEj39/q1vLjJmsk2riFBUQwZFPzwK4HRgY7O11sraxzhJ0HfrUbda2ngT
            xmvuumrL7tgtUIE0doL2PI1RrGxotOxn7yb/Sxyq0YXZuCejP3TyLJeQ3iF4sikB
            pUtyuFeNAc8n2rYTd1DIPEu2rmRoJvXDseZ7uSGWhDTvT0oujFhclH9sRmBNQ6gr
            Ofp4XBg5zWG25eqADYPWPJ/xCs6qKqcnNndQFvPOEOru4y4DlzrhVr/aaoUzEcZa
            Y+D0BIVFkuACck8ME90BaQfuzkb8dcnyk/u0s+PpJINSAK8swuwhw/IV0JqGbfVp
            00S4sfr782S2DFAL/fAyvOyam2qChdCY+zKFYUUe0VebViEKtDQpOSEhfG5myx+d
            Qzzt6sHpa2H/1lNyrBlevORonWVaevrVo4vEL43ffdVt5Ywk+tsCn2miY/LmkQPr
            jh1EyvuHR/wy5BVIQLZylysxo7ZOUqWNAM4d8lo5FJUOMW6+EZuQmCYLOvYvQJTS
            XAH8n7J+Gl+VO2WZoAtO8z+qnDYOwJBGRCD5WVlaYwJDWpUZro2XuBSwUki8J/nR
            6a9hNeuQi4kL96wWdtj8EfbsaLCqrLkOlQ5hSVwU+Yw1DtydatKy6LcJTUr2
            =r6p/
            -----END PGP MESSAGE-----
          fp: BCE1F3023A9ABFCC2561AB34CA7CB93D9744872D
    encrypted_regex: ^(password)$
    version: 3.10.2
  1. The file can be decrypted as follows
$ sops decrypt kubernetes-secret.yaml

apiVersion: v1
kind: Secret
metadata:
    name: database-password
type: Opaque
data:
    password: secret-database-password

The process above works equally well when using SOPS interactively on the command line or programmatically in a CI/CD pipeline. Overall I have been very happy with SOPS.