Skip to content

Set up SOPS encryption

In this tutorial, you'll set up SOPS encryption for the repository from Protect a container, so secrets can live in it encrypted to your key and to web01's. Then you'll wire secret files into the CUE config. The next tutorial adds the first secret.

See Secret encryption for how the keys fit together.

Prerequisites

  • The infra repository and web01.example.com from Protect a container.
  • sops, age and ssh-to-age on your workstation.
  • An ed25519 SSH host key on the server at /etc/ssh/ssh_host_ed25519_key, which most distributions create on install.

1. Create your admin key

Generate an age key where sops looks for it by default:

Bash
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt

On macOS, sops looks in ~/Library/Application Support/sops/age/keys.txt instead; either use that path or point SOPS_AGE_KEY_FILE at the file. If you already have a key, skip this step.

age-keygen prints the public key, which starts with age1. Print it again at any time with:

Bash
age-keygen -y ~/.config/sops/age/keys.txt

Keep the private key out of the repository and back it up; without it, you can't edit your secrets anymore.

2. Get the server's public key

Convert the server's public SSH host key to an age recipient:

Bash
ssh web01.example.com cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age

This also prints a public key starting with age1. syslet performs the same conversion on the private half, so only web01 can decrypt what you encrypt to this key.

3. Configure sops

Create .sops.yaml in the repository root, with the two public keys from above:

.sops.yaml
1
2
3
4
5
6
7
8
9
keys:
  - &admin age1...   # your admin key
  - &web01 age1...   # web01's host key
creation_rules:
  - path_regex: .*\.enc\.yaml$
    key_groups:
      - age:
          - *admin
          - *web01

sops applies this rule to every file ending in .enc.yaml, so you never pass recipients on the command line. When you add a server or an admin, add its key here and run sops updatekeys on each existing file.

4. Load secret files in CUE

To create secrets on the host, syslet needs a secret spec for each encrypted file. From it, syslet creates one podman secret per key, and containers refer to those podman secrets by name.

CUE can't read files by default. Allow it by adding the @extern(embed) attribute above the package line of syslet.cue, then add the secretFiles and sysdef: secrets lines before syslet: specRendered:

syslet.cue
@extern(embed)

package syslet

import (
    sysletcore "github.com/xchangeee/syslet/schema/core@v0"
    syslettools "github.com/xchangeee/syslet/schema/tools@v0"
)

// Host that cue cmd plan/apply deploy to
fqdn: "web01.example.com"

#Sysdef: {
    sysletcore.#Sysdef
    sysletcore.#SysdefDefaults
}

sysdef: #Sysdef

// Encrypted secret files, keyed by file name
secretFiles: _ @embed(glob=creds-*.enc.yaml,type=text,allowEmptyGlob)

sysdef: secrets: (syslettools.#SysdefSecretsFromEmbeddedFiles & {in: secretFiles}).secrets

syslet: specRendered: (syslettools.#SysletJsonFromSysdef & {in: sysdef}).specRendered

secretFiles reads every creds-<name>.enc.yaml next to syslet.cue, and sysdef: secrets turns each file into a secret spec called <name>.

@embed reads the files as text, still encrypted. CUE needs no age key for this, so cue vet and cue export also work in CI or for anyone without access to the secrets. allowEmptyGlob keeps the config valid when there are no secret files. #SysdefSecretsFromEmbeddedFiles strips the creds- prefix and .enc.yaml suffix to get the spec name, so creds-site.enc.yaml becomes the secret spec site.

There are no secret files yet, so nothing changes on the host:

Bash
cue cmd plan

Commit the setup:

Bash
git add -A && git commit -m "set up sops"

Continue with Add nginx basic auth.