digitalundso

Managing secrets with style and SOPS

April 22, 2025

Sooner or later you will face the problem to store secrets in your IaC-repo. And maybe you’re also hosting this repo on a public git-Server like codeberg.org. Is it a bad idea to store secrets in plain text in a public repository? Hell yes! But there are a lot of solutions to this problem.

One solution I like to use is SOPS. SOPS itself is not an encryption algorithm or encryption tool itself but rather uses different existing encryption tools like PGP, age, OpenBao/Hashicorp Vault, Azure Key Vault, GCP KMS or AWS KMS to encrypt YAML, JSON, ENV, INI or binary file formats. One thing to keep in mind is that SOPS only encrypts the value part of a key-value pair! This reflects in the use of the before mentioned file formats.

SOPS & age

As mentioned, SOPS can use different encryption algorithms. One of those is age, “[..] a simple, modern and secure file encryption tool, format, and Go library. It features small explicit keys, no config options, and UNIX-style composability."

$ age-keygen -o age-key.txt

This will create a file called age-key.txt that contains the public and private part of your new age encryption key. You can share the public part of your key much like a public SSH key but more on that later.

SOPS & OpenBao

Ich werde an dieser Stelle nicht auf die Installation und Konfiguration von OpenBao eingehen. Details sind hier zu finden.

Im Grunde funktioniert die Integration von SOPS und OpenBao genau so, wie bei SOPS und age nur mit dem Unterschied, dass mein Key zum ver- und entschlüsseln nicht lokal auf meinem PC liegt, sondern mir durch OpenBao bereitgestellt wird. Es wird nämlich die Funktionalität der sogenannten Transit-Keys genutzt, auf die SOPS mittels API zugreifen kann.

SOPS basics

Für mich stellt SOPS eine großartige Lösung dar, da es mir ermöglicht, Secrets kontextabhängig verwalten und einsetzen zu können sowie die Verwaltung der Secrets in einem Team effizient zu steuern.

Möchte ich SOPS einsetzen in Rahmen eines Projektes, wird eine .sops.yaml Datei benötigt, die in der einfachsten Art und Weise wie folgt aussehen kann:

creation_rules:
    - age: age1npayaryxhujm49j9mhgrl0a76x97pl5g504eskzpsd3qjhsv74hq28py8t

Hiermit wird definiert, dass alle Dateien mit einem age-Key von SOPS ver- bzw. entschlüsselt werden sollen. Der Wert hinter - age: ist der öffentliche Teil des age-Keys.

Damit aber noch nicht genut. Mithilfe der Datei .sops.yaml kann ich auch komplexere Regeln aufbauen, die auf Datei- oder Ordnernamen basieren:

creation_rules:
    -   path_regex: \.dev\.yaml$
        age: age12dff5uxteqzf530y9nhjeqkzylxtjr2rnx76mq5re77cwkpupchq42adgz
    -   path_regex: \.prod\.yaml$
        age: age1tewuayxvre4n35kqe6espx4fyyjk0u2c7n7lpautum2egd7uvedqw2k4en
    -   path_regex: .*/development/.*
        hc_vault_transit_uri: "http://localhost:8200/v1/sops/keys/secondkey"
    -   age: age1npayaryxhujm49j9mhgrl0a76x97pl5g504eskzpsd3qjhsv74hq28py8t

Dieses Beispiel zeigt, dass Dateien, die den Suffix .dev.yaml (also z.B. secrets.dev.yaml) enthalten, mithilfe des age-Keys mit der Endung adgz ver- bzw. entschlüsselt werden und Dateien, die den Suffix .prod.yaml enthalten, entsprechend mit dem age-Key mit der Endung k4en. Mithilfe der Zeile path_regex: .*/development/.* geben wir an, dass alle Dateien im Ordner development - unabhängig vom Namen und der Dateiendung durch mit einen transit-Key von OpenBao ver- und entschlüsselt werden sollen.

Über den Eintrag in der letzten Zeile weisen wir SOPS an, für alle anderen Dateien den Key der mit py8t endet zu nutzen.

Ansible

Für das Management von Secrets in Ansible gibt es eine Vielzahl unterschiedlicher Optionen. Die naheliegenste ist natürlich die Nutzung von Ansible Vault. Der große Vorteil von Ansible Vault ist, dass es direkt bei jeder Installation von Ansible direkt mit dabei ist und daher sofort einsatzbereit ist. Grundsätzlich spricht nichts gegen den Einsatz von Ansible Vault, da es bestens in das Ökosystem von Ansible eingebunden ist. Aber sobald man anfängt, Secrets in größeren Teams gemeinsam zu verwalten, merkt man schnell, das Ansible Vault eher für einen überschaubaren Einsatz gedacht ist (so zumindest meine Einschätzung).

Using SOPS without a Ansible module

[defaults]
vars_plugins_enabled = host_group_vars,community.sops.sops

What will happen if you want to interact with the variables but are not allowed (or forgot to set up you age keys properly)?

$ ansible-inventory --list
ERROR! error with file /home/seike/Repos/sops_demo/host_vars/localhost.sops.yml: CouldNotRetrieveKey exited with code 128: Failed to get the data key required to decrypt the SOPS file.

Group 0: FAILED
  age1npayaryxhujm49j9mhgrl0a76x97pl5g504eskzpsd3qjhsv74hq28py8t: FAILED
    - | failed to create reader for decrypting sops data key with
      | age: no identity matched any of the recipients

Recovery failed because no master key was able to decrypt the file. In
order for SOPS to recover the file, at least one key has to be successful,
but none were.

In case of a dynamic Ansible inventory, the SOPS module is not of much help here. Due to the fact, that modules are only used at the role or play level, anything before can’t benefit from features of the module.

In the case of the Hetzner inventory plugin, the easiest way to provide the API key is to use it as an environment variable HCLOUD_TOKEN.

$ sops exec-env secrets.sops.yaml 'ansible-inventory --list'
$ sops exec-env secrets.sops.yaml 'export HCLOUD_TOKEN=$hetzner_token; ansible-inventory --list'

Blockquote

And bold, italics, and even *italics and later bold*. Even strikethrough. A link to somewhere.

And code highlighting:

var foo = 'bar';

function baz(s) {
   return foo + ':' + s;
}

Or inline code like var foo = 'bar';.

Or an image of bears

bears

The end …