Encryption
You encrypt secret files with age for one or more recipients. Each recipient is identified by a public key, while the matching private key is needed to decrypt the file. Copland decrypts these files during activation.
Recipients and keys
By default, Copland uses the private SSH host key at /etc/ssh/ssh_host_ed25519_key to decrypt secrets during activation. You use the corresponding public key at /etc/ssh/ssh_host_ed25519_key.pub to encrypt files for this machine. The private key stays on the machine.
Multiple keys
You can specify multiple private keys for decrypting your secrets.
{
age.identityPaths = [
"/etc/ssh/ssh_host_ed25519_key"
"/var/lib/copland/age/identity.txt"
];
}
Encrypting a secret
Create token.txt and put the secret inside it, such as an API token needed by a service. Encrypt that file with the machine's public SSH host key:
age -R /etc/ssh/ssh_host_ed25519_key.pub -o token.age token.txt
The command encrypts the contents of token.txt and writes the result to token.age. The public key selects the recipient, and the matching private key lets Copland decrypt the file during activation. Neither key is being encrypted by this command.
Using secrets in services
The service module declares the encrypted file and passes the decrypted file's path to the program. This example uses a placeholder daemon foo that accepts a --token-file argument.
Directory structure
Keep token.age beside the module that uses it:
hosts/<host>/
├── default.nix
└── services/
├── default.nix
└── foo/
├── default.nix
└── token.age
Place the encrypted token.age from the previous step in services/foo/. The plaintext token.txt is not part of this configuration. Add ./foo to the imports in services/default.nix, and make sure the host's default.nix imports ./services.
Service declaration
Put this module in services/foo/default.nix:
{ config, pkgs, ... }:
{
users.groups.foo = { };
users.users.foo = {
isSystemUser = true;
group = "foo";
};
age.secrets."foo-token" = {
file = ./token.age;
owner = "foo";
group = "foo";
mode = "0400";
restartServices = [ "foo" ];
};
openrc.services.foo = {
command = "${pkgs.foo}/bin/foo --token-file ${config.age.secrets."foo-token".path}";
user = "foo";
group = "foo";
runlevel = config.openrc.defaultRunlevel;
respawn = true;
};
}
./token.age is relative to this module. The secret name foo-token determines the default decrypted path, /run/secrets/foo-token.
Reading the secret
During activation, Copland decrypts token.age and installs it at /run/secrets/foo-token, owned by foo with mode 0400. The service runs as foo and reads the token from that file through its --token-file argument. Copland passes the path, not the token itself, in the command line.
Build and switch the system to apply the declaration. When the encrypted file changes, restartServices makes Copland restart foo during the switch so it reads the updated token. Replace the placeholder package, command and argument with those supported by your actual service.