banner

Secrets with SOPS & Age

Last updated 

Photo Credits: Unsplash and SBTS2018 on FlatIcon

Definitions

If you're not familiar with the concept of Secrets Management, here's a quick definition from Imperva:

Secret management is a practice that allows developers to securely store sensitive data such as passwords, keys, and tokens, in a secure environment with strict access controls.

For small software projects, secret management can be simple to achieve. But as teams and software codebases grow, there are additional secrets spread across an application ecosystem, making them more difficult to manage. Microservices, development tools, containers, orchestrators, and API connections all require secrets to perform their functions, and these must be stored and delivered in a secure manner.

It is very common to have secrets hard-coded into scripts, configurations, or source code, making them easily accessible to attackers. Secret management solutions can ensure that sensitive information is never embedded into any artifact in plaintext, saving secrets separately from code, and providing an audit trail by enforcing privilege-based sessions for all access attempts.

Introduction

Most developers - and homelab admins - would agree that storing credentials in code is less than ideal.

However, many - or, at least, me - have been skeptical about implementing secrets management not because of its usefulness but because of its value proposition: the freely available solutions are, for the most part, massively more complex than what I need. HashiCorp Vault is a fantastic tool, but I don't want to run it in my homelab for casual and infrequent use - especially if the code I'm working on is actually the provisioning material for the homelab itself (see: "chicken and egg").

Fortunately, a friend has recently introduced me to a lightweight, convenient, and open source solution: SOPS + Age.

About The Tools

SOPS: Secrets OPerationS

SOPS is "an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, age, and PGP."

I use SOPS for two reasons:

  1. Convenience. Once configured, you can use SOPS to create or edit encrypted files without running a single encrypt/decrypt command. Just sops filename.
  2. Automation. There's a fantastic Terraform provider for SOPS. (If the value of that isn't immediately clear, stay tuned for a future article on how I integrate the two.)

DEK/KEK Encryption

SOPS uses a DEK/KEK model for encryption. When it creates or encrypts a file, it does so with a randomly-generated 256 bit data encryption key (DEK), then encrypts that data key with the configured or specified key encryption key(s) (KEK).

Multiple KEKs can be used, allowing multiple private keys to decrypt the secrets. This is accomplished by storing multiple encrypted copies of the DEK in the encrypted data file, each associated with its own public key.

FiloSottile/age

age is "a simple, modern and secure file encryption tool, format, and Go library."

SOPS can use several different key/encryption providers, including age. Most of the rest are cloud-based, e.g. AWS KMS. age is local, lightweight, and extremely easy to set up.

Testing it Out

Start a Dev Environment

To proactively keep my host system 'clean,' I do all of my testing in containers or VMs. Feel free to substitute your own approach.

root@host:/> docker run -it debian:latest /bin/bash

Install Tools

root@2b92d1f7368b:/:> apt update && apt install curl vim -y

# Output Removed here, and elsewhere when unnecessary

root@2b92d1f7368b:/:> cd /opt

# Download the binary - check for the latest one, of course!
root@2b92d1f7368b:/opt:> curl -LO https://github.com/getsops/sops/releases/download/v3.8.0-rc.1/sops-v3.8.0-rc.1.linux.amd64

# Download the checksums file, certificate and signature
root@2b92d1f7368b:/opt:> curl -LO https://github.com/getsops/sops/releases/download/v3.8.0-rc.1/sops-v3.8.0-rc.1.checksums.txt

root@2b92d1f7368b:/opt:> curl -LO https://github.com/getsops/sops/releases/download/v3.8.0-rc.1/sops-v3.8.0-rc.1.checksums.pem

root@2b92d1f7368b:/opt:> curl -LO https://github.com/getsops/sops/releases/download/v3.8.0-rc.1/sops-v3.8.0-rc.1.checksums.sig

root@2b92d1f7368b:/opt:> sha256sum -c sops-v3.8.0-rc.1.checksums.txt --ignore-missing
sops-v3.8.0-rc.1.linux.amd64: OK

# Download cosign for even more verification
root@2b92d1f7368b:/opt:> LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest \
| grep tag_name | cut -d : -f2 | tr -d "v\", ")

root@2b92d1f7368b:/opt:> curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb"

root@2b92d1f7368b:/opt:> dpkg -i cosign_${LATEST_VERSION}\_amd64.deb

root@2b92d1f7368b:/opt:> cosign verify-blob sops-v3.8.0-rc.1.checksums.txt --certificate sops-v3.8.0-rc.1.checksums.pem \
--signature sops-v3.8.0-rc.1.checksums.sig --certificate-identity-regexp=https://github.com/getsops \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
Verified OK

# Move the binary in to your PATH
root@2b92d1f7368b:/opt:> mv sops-v3.8.0-rc.1.linux.amd64 /usr/local/bin/sops

# Make the binary executable
root@2b92d1f7368b:/opt:> chmod +x /usr/local/bin/sops

root@2b92d1f7368b:/opt:> apt install age -y

root@2b92d1f7368b:/opt:> mkdir -p ~/.config/sops/age

root@2b92d1f7368b:/opt:> age-keygen -o ~/.config/sops/age/keys.txt
Public key: age1rwrumcadz6jk99j2gxjw3nsywdcyuwyc9858gqg5k3wvq8w6gqvqn8qj47

root@2b92d1f7368b:/opt:> cat ~/.config/sops/age/keys.txt
# created: 2023-08-26T00:16:25Z
# public key: age1rwrumcadz6jk99j2gxjw3nsywdcyuwyc9858gqg5k3wvq8w6gqvqn8qj47
AGE-SECRET-KEY-REDACTEDKEYHERE

Using SOPS+AGE

# Create a config file so SOPS knows what key to use
root@2b92d1f7368b:/opt:> cat \<\< EOF \> .sops.yaml
> creation_rules:
>   - age: "age1rwrumcadz6jk99j2gxjw3nsywdcyuwyc9858gqg5k3wvq8w6gqvqn8qj47"
> EOF
# obviously - use your own public key

root@2b92d1f7368b:/opt:> sops test.yaml

SOPS will then drop you into vim, with the following yaml template:

hello: Welcome to SOPS! Edit this file as you please!
example_key: example_value
# Example comment
example_array:
  - example_value1
  - example_value2
example_number: 1234.56789
example_booleans:
  - true
  - false

Note: It's possible to create and edit "raw" (i.e. unstructured) files with SOPS. However, with specific, structured filetypes (i.e., yaml) it's capable of "targeted" encryption. Essentially, it will only encrypt the values of our yaml file and not the keys. From the README:

sops uses the file extension to decide which encryption method to use on the file content. YAML, JSON, ENV, and INI files are treated as trees of data, and key/values are extracted from the files to only encrypt the leaf values. The tree structure is also used to check the integrity of the file.

After saving the file, you can view the encrypted version with a cat test.yaml:

hello: ENC[AES256_GCM,data:fNNoqMbZLW2n5vz4nd9KyBPpkGbdJTE7YZZeH4sJ6t3/GRlIF/QzEnD7PndClQ==,iv:2li3yVay4KBGK/EgFoDD4vXtAffnffruGJj8huUMFZo=,tag:f60yzaNqSL2KYJHpSX82og==,type:str]
example_key: ENC[AES256_GCM,data:Wt03RCvFzBHfapwFIw==,iv:e9o8thLfXbsc76C0FPLyQNBaDyChBcbtby/WnPYApIg=,tag:RM5NEUYM2UK+hNXcuOVyzA==,type:str]
#ENC[AES256_GCM,data:kqFjXMan8Dn1qRYQ2k/hCw==,iv:N/2QmyeoGqzuw7w35FWiC53jYoRJAFnho9C0s91DCZM=,tag:KWOtfFmraQBfCoUK2mnsjw==,type:comment]
example_array:
  - ENC[AES256_GCM,data:55u6LOQSVag/7uxm8L0=,iv:tDlUjrMSNaNCtbT6wrL8zEWLiVO/VDc8N08cLbv033o=,tag:1W001s6bNZDemofAVDYIzw==,type:str]
  - ENC[AES256_GCM,data:GU8Ur24YxlA0EQtcIy4=,iv:lNYb9YKlDTfZ2g7+qFrYwaQnO5eWn2ulu+/UItkfr14=,tag:qZy8qqKNbD5O5c2i6lwY3Q==,type:str]
example_number: ENC[AES256_GCM,data:WewtHzjhHj5FBQ==,iv:dfmOOYAOPIV0cVnc6E5S4VaEjb+MU+DZhaP7Ldi9DVI=,tag:t0H09QMUieIjKXe0MZALpw==,type:float]
example_booleans:
  - ENC[AES256_GCM,data:A+6Flg==,iv:/5qpoHX70v3X4hywCBvvzXVQQLYNKFactbkwite+EHs=,tag:CKgbO5lNXIo9GYLoMrOPhQ==,type:bool]
  - ENC[AES256_GCM,data:KDYY+i8=,iv:G/hEXLZ8G9b6CHjajWkHNm0XIO7vRrxsE+bOS8joPQU=,tag:z0Bp71+2+4gxyrJkqLLHbg==,type:bool]
sops:
  kms: []
  gcp_kms: []
  azure_kv: []
  hc_vault: []
  age:
    - recipient: age1rwrumcadz6jk99j2gxjw3nsywdcyuwyc9858gqg5k3wvq8w6gqvqn8qj47
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWMUtBcStYdCt4TGkrYlVL
        MkhIWlIwVzBNRkFqb3pZd2dNaE9ib2JQYm5VCnZCUURvdmJFS01CSE9qbTcvVnhw
        Nlk4QjQ0SXdKVVo4RjF4MkFPckFIaTgKLS0tIFFSeVhxZEZ4cGpLeGsrWnpQMEg2
        eDR0Z1hCZjdmUFdoaHlyS1ZsdTV6Mm8KxskZTV+o2yJ506pijDfXYHPeNuLkeSsD
        qJnq1M5A5/WnfKBn29AuthMTP45oo4zj3rFB3JQnqipaken0YijhTQ==
        -----END AGE ENCRYPTED FILE-----
  lastmodified: '2023-08-29T00:28:56Z'
  mac: ENC[AES256_GCM,data:ykzKTV5NPo547Uywc+3NhrrOhuyc3FoxmZH0DdAV2fpSSYYFGilCTWwvsS1QcRFaI4XCUtzVNu9pnhvNNkbn9soHcjQNCW4lB08rNVjBm2fCYh+dkULWzeG52NZT+szbynmAvxYcgitnu+pBB4h/oZ+BvJLLb9LrOiXQXu6TH4E=,iv:jzfxc0N13DDLiAUV+cWj4E+86OsUSijDUdGVE3XDcZc=,tag:PfAdq92sSmXEQz8ESQHjxg==,type:str]
  pgp: []
  unencrypted_suffix: _unencrypted
  version: 3.8.0-rc.1

You can always edit the file again with sops test.yaml, or decrypt it to stdout with sops -d test.yaml.

Conclusion

And thus, you can securely store your encrypted secrets within your repositories (assuming you keep that age key where it belongs!).

SOPS + age will become even more useful when used with tools like Terraform that support integrations. In the case of Terraform - once the provider is configured - a simple terraform apply will call sops in the background to fetch specified data. But... more on that later.

Stay tuned!