From 7e035c5ee0c2e60aad90da1cbb1072266a3996fd Mon Sep 17 00:00:00 2001 From: lucspec Date: Fri, 27 Feb 2026 15:07:59 -0700 Subject: [PATCH] my own guide for ssh-to-age --- README.md | 399 ++++-------------------- docs/HowTo_sshToAge.md | 198 ++++++++++++ docs/InDepthUsage.md | 686 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 939 insertions(+), 344 deletions(-) create mode 100644 docs/HowTo_sshToAge.md create mode 100644 docs/InDepthUsage.md diff --git a/README.md b/README.md index 328be60..1f3614e 100644 --- a/README.md +++ b/README.md @@ -33,28 +33,11 @@ environment variables that can be passed to sops. There is a `configuration.nix` example in the [deployment step](#deploy-example) of our usage example. -## Supported encryption methods +## Installation -sops-nix supports two basic ways of encryption, GPG and `age`. +Choose one of the following methods. When using it non-globally with home-manager, refer to [Use with home-manager](./docs/InDepthUsage.md#use-with-home-manager). -GPG is based on [GnuPG](https://gnupg.org/) and encrypts against GPG public keys. Private GPG keys may -be used to decrypt the secrets on the target machine. The tool [`ssh-to-pgp`](https://github.com/Mic92/ssh-to-pgp) can -be used to derive a GPG key from a SSH (host) key in RSA format. - -The other method is `age` which is based on [`age`](https://github.com/FiloSottile/age). -The tool ([`ssh-to-age`](https://github.com/Mic92/ssh-to-age)) can convert SSH host or user keys in Ed25519 -format to `age` keys. - -## Usage example - -If you prefer video over the textual description below, you can also checkout this [6min tutorial](https://www.youtube.com/watch?v=G5f6GC7SnhU) by [@vimjoyer](https://github.com/vimjoyer). - -
-1. Install sops-nix - -Choose one of the following methods. When using it non-globally with home-manager, refer to [Use with home-manager](#use-with-home-manager). - -#### Flakes (current recommendation) +### Flakes (current recommendation) If you use experimental nix flakes support: @@ -77,9 +60,14 @@ If you use experimental nix flakes support: } ``` -##### [`nix-darwin`](https://github.com/nix-darwin/nix-darwin) +### Alternative Installs -A module for `nix-darwin` is also available for global install with flakes: +
+Nix Darwin (MacOS Users) + +See [`nix-darwin`](https://github.com/nix-darwin/nix-darwin) for background info. + +A module for `nix-darwin` is available for global install with flakes: ```nix { @@ -99,9 +87,13 @@ A module for `nix-darwin` is also available for global install with flakes: }; } ``` +
+
+Niv (recommended if not using flakes) -#### [`niv`](https://github.com/nmattia/niv) (recommended if not using flakes) - First add it to niv: +See [`niv`](https://github.com/nmattia/niv) (recommended if not using flakes) + +First add it to niv: ```console $ niv add Mic92/sops-nix @@ -114,10 +106,11 @@ $ niv add Mic92/sops-nix imports = [ "${(import ./nix/sources.nix).sops-nix}/modules/sops" ]; } ``` - -#### `fetchTarball` +
+
+fetchTarball (None of the above) - Add the following to your `configuration.nix`: +Add the following to your `configuration.nix`: ```nix { @@ -133,339 +126,57 @@ $ niv add Mic92/sops-nix ]; } ``` -
-
-2. Generate a key for yourself +## Usage -This key will be used for you to edit secrets. +`sops-nix` supports two basic ways of encryption, GPG and `age`. -You can generate yourself a key: +First, pick your track. If you have time, there are some excellent resources on each key type that we won't cover here. -```console -# for age.. -$ mkdir -p ~/.config/sops/age -$ age-keygen -o ~/.config/sops/age/keys.txt -# or to convert an ssh ed25519 key to an age key -$ mkdir -p ~/.config/sops/age -$ nix-shell -p ssh-to-age --run "ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt" -# for GPG >= version 2.1.17 -$ gpg --full-generate-key -# for GPG < 2.1.17 -$ gpg --default-new-key-algo rsa4096 --gen-key -``` +1. [`age`](https://github.com/FiloSottile/age) +2. [GnuPG](https://gnupg.org/) +3. SSH + - [`ssh-to-age`](https://github.com/Mic92/ssh-to-age) (automatic conversion) + - [`ssh-to-pgp`](https://github.com/Mic92/ssh-to-pgp) (manual conversion) -Or you can use the `ssh-to-pgp` tool to get a GPG key from an SSH key: -```console -$ nix-shell -p gnupg -p ssh-to-pgp --run "ssh-to-pgp -private-key -i $HOME/.ssh/id_rsa | gpg --import --quiet" -2504791468b153b8a3963cc97ba53d1919c5dfd4 -# This exports the public key -$ nix-shell -p ssh-to-pgp --run "ssh-to-pgp -i $HOME/.ssh/id_rsa -o $USER.asc" -2504791468b153b8a3963cc97ba53d1919c5dfd4 -``` -(Note that `ssh-to-pgp` only supports RSA keys; to use Ed25519 keys, use `age`.) -If you get the following, -```console -ssh-to-pgp: failed to parse private ssh key: ssh: this private key is passphrase protected -``` -then your SSH key is encrypted with your password and you will need to create an unencrypted copy temporarily. -```console -$ cp $HOME/.ssh/id_rsa /tmp/id_rsa -$ ssh-keygen -p -N "" -f /tmp/id_rsa -$ nix-shell -p gnupg -p ssh-to-pgp --run "ssh-to-pgp -private-key -i /tmp/id_rsa | gpg --import --quiet" -$ rm /tmp/id_rsa -``` +```mermaid +--- +config: + look: classic + theme: dark + layout: dagre +--- +graph TD; +Start[Start] -When using `nix-darwin` save the `age` key to `$HOME/Library/Application Support/sops/age/keys.txt` or set a [custom](https://github.com/getsops/sops#23encrypting-using-age) configuration directory. +ageLink[Age How-To] +click ageLink href "./docs/HowTo_age.md" -
- How to find the public key of an `age` key +sshLink[SSH How-To] +click ageLink href "./docs/HowTo_ssh.md" -If you generated an `age` key, the `age` public key can be found via `age-keygen -y $PATH_TO_KEY`: -```console -$ age-keygen -y ~/.config/sops/age/keys.txt -age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl -``` +gpgLink[GPG How-To] +click ageLink href "./docs/HowTo_gpg.md" -Otherwise, you can convert an existing SSH key into an `age` public key: -```console -$ nix-shell -p ssh-to-age --run "ssh-to-age < ~/.ssh/id_ed25519.pub" -# or -$ nix-shell -p ssh-to-age --run "ssh-add -L | ssh-to-age" -``` +Decision1{I have opinions +on key types} +Start --> Decision1 -
+Decision1 -->|No| Decision2{I already +use SSH} -
- How to find the GPG fingerprint of a key +Decision1 -->|Yes| Know{Click +a tutorial} -Invoke this command and look for your key: -```console -$ gpg --list-secret-keys -/tmp/tmp.JA07D1aVRD/pubring.kbx -------------------------------- -sec rsa2048 1970-01-01 [SCE] - 9F89C5F69A10281A835014B09C3DC61F752087EF -uid [ unknown] root -``` +Know -->|Age| ageLink +Know -->|SSH| sshLink +Know -->|GPG| gpgLink -The fingerprint here is `9F89C5F69A10281A835014B09C3DC61F752087EF`. -
- -Your `age` public key or GPG fingerprint can be written to your [`.sops.yaml`](https://github.com/getsops/sops#using-sops-yaml-conf-to-select-kms-pgp-and-age-for-new-files) in the root of your configuration directory or repository: -```yaml -# This example uses YAML anchors which allows reuse of multiple keys -# without having to repeat yourself. -# Also see https://github.com/Mic92/dotfiles/blob/d6114726d859df36ccaa32891c4963ae5717ef7f/nixos/.sops.yaml -# for a more complex example. -keys: - - &admin_alice 2504791468b153b8a3963cc97ba53d1919c5dfd4 - - &admin_bob age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl -creation_rules: - - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ - key_groups: - - pgp: - - *admin_alice - age: - - *admin_bob -``` - -**Note:** -Be sure to not include a `-` before subsequent key types under `key_groups` -(i.e. `age` in the above example should not have a `-` in front). -This will otherwise cause sops to require multiple keys (shamir secret sharing) -to decrypt a secret, which breaks normal sops-nix usage. - -
- -
-3. Get a public key for your target machine - -The easiest way to add new machines is by using SSH host keys (this requires OpenSSH to be enabled). - -If you are using `age`, the `ssh-to-age` tool can be used to convert any SSH Ed25519 public key to the `age` format: -```console -$ nix-shell -p ssh-to-age --run 'ssh-keyscan example.com | ssh-to-age' -age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 -$ nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' -age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 -``` - -For GPG, since sops does not natively support SSH keys yet, sops-nix supports a conversion tool (`ssh-to-pgp`) to store them as GPG keys: - -```console -$ ssh root@server01 "cat /etc/ssh/ssh_host_rsa_key" | nix-shell -p ssh-to-pgp --run "ssh-to-pgp -o server01.asc" -# or with sudo -$ ssh youruser@server01 "sudo cat /etc/ssh/ssh_host_rsa_key" | nix-shell -p ssh-to-pgp --run "ssh-to-pgp -o server01.asc" -0fd60c8c3b664aceb1796ce02b318df330331003 -# or just read them locally/over ssh -$ nix-shell -p ssh-to-pgp --run "ssh-to-pgp -i /etc/ssh/ssh_host_rsa_key -o server01.asc" -0fd60c8c3b664aceb1796ce02b318df330331003 -``` - -The output of these commands is the identifier for the server's key, which can be added to your `.sops.yaml`: - -```yaml -keys: - - &admin_alice 2504791468b153b8a3963cc97ba53d1919c5dfd4 - - &admin_bob age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl - - &server_azmidi 0fd60c8c3b664aceb1796ce02b318df330331003 - - &server_nosaxa age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 -creation_rules: - - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ - key_groups: - - pgp: - - *admin_alice - - *server_azmidi - age: - - *admin_bob - - *server_nosaxa - - path_regex: secrets/azmidi/[^/]+\.(yaml|json|env|ini)$ - key_groups: - - pgp: - - *admin_alice - - *server_azmidi - age: - - *admin_bob -``` - -If you prefer having a separate GPG key, see [Use with GPG instead of SSH keys](#use-with-GPG-instead-of-SSH-keys). - -
- -
-4. Create a sops file - -To create a sops file you need write a `.sops.yaml` as described above. - -When using GnuPG you also need to import your personal GPG key -(and your colleagues) and your servers into your GPG key chain. - -
-sops-nix can automate the import of GPG keys with a hook for nix-shell, allowing public -keys to be shared via version control (i.e. git). - -```nix -# shell.nix -with import {}; -let - sops-nix = builtins.fetchTarball { - url = "https://github.com/Mic92/sops-nix/archive/master.tar.gz"; - }; -in -mkShell { - # imports all files ending in .asc/.gpg - sopsPGPKeyDirs = [ - "${toString ./.}/keys/hosts" - "${toString ./.}/keys/users" - ]; - # Also single files can be imported. - #sopsPGPKeys = [ - # "${toString ./.}/keys/users/mic92.asc" - # "${toString ./.}/keys/hosts/server01.asc" - #]; - - # This hook can also import gpg keys into its own seperate - # gpg keyring instead of using the default one. This allows - # to isolate otherwise unrelated server keys from the user gpg keychain. - # By uncommenting the following lines, it will set GNUPGHOME - # to .git/gnupg. - # Storing it inside .git prevents accedentially commiting private keys. - # After setting this option you will also need to import your own - # private key into keyring, i.e. using a a command like this - # (replacing 0000000000000000000000000000000000000000 with your fingerprint) - # $ (unset GNUPGHOME; gpg --armor --export-secret-key 0000000000000000000000000000000000000000) | gpg --import - #sopsCreateGPGHome = true; - # To use a different directory for gpg dirs set sopsGPGHome - #sopsGPGHome = "${toString ./.}/../gnupg"; - - nativeBuildInputs = [ - (pkgs.callPackage sops-nix {}).sops-import-keys-hook - ]; -} -``` - -A valid directory structure for this might look like: - -```console -$ tree . -. -├── keys -│   ├── hosts -│   │   └── server01.asc -│   └── users -│   └── mic92.asc -``` - -
- -After configuring `.sops.yaml`, you can open a new file with sops: - -```console -$ nix-shell -p sops --run "sops secrets/example.yaml" -``` - -This will start your configured editor located at the `$EDITOR` environment variable. -An example secret file might be: -```yaml -# Files must always have a string value -example-key: example-value -# Nesting the key results in the creation of directories. -# These directories will be owned by root:keys and have permissions 0751. -myservice: - my_subdir: - my_secret: password1 -``` - -An example result when saving this file could be: +Decision2 -->|Yes| sshLink +Decision2 -->|No| ageLink ``` -example-key: ENC[AES256_GCM,data:AB8XMyid4P7mXdjj+A==,iv:RRsZC+V+3w22pOi/2TCjBYn/0OYsNGCu5CT1ZBSKGi0=,tag:zT5mlujrSuA6KKxLKL8CMQ==,type:str] -#ENC[AES256_GCM,data:59QWbzCQCP7kLdhyjFOZe503MgegN0kv505PBNHwjp6aYztDHwx2N9+A1Bz6G/vWYo+4LpBo8/s=,iv:89q3ZXgM1wBUg5G29ROor3VXrO3QFGCvfwDoA3+G14M=,tag:hOSnEZ6DKycnF37LCXOjzg==,type:comment] -#ENC[AES256_GCM,data:kUuJCkDE9JT9C+kdNe0CSB3c+gmgE4We1OoX4C1dWeoZCw/o9/09CzjRi9eOBUEL0P1lrt+g6V2uXFVq4n+M8UPGUAbRUr3A,iv:nXJS8wqi+ephoLynm9Nxbqan0V5dBstctqP0WxniSOw=,tag:ALx396Z/IPCwnlqH//Hj3g==,type:comment] -myservice: - my_subdir: - my_secret: ENC[AES256_GCM,data:hcRk5ERw60G5,iv:3Ur6iH1Yu0eu2otcEv+hGRF5kTaH6HSlrofJ5JXvewA=,tag:hpECXFnMhGNnAxxzuGW5jg==,type:str] -sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] - age: - - recipient: age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1dFYvSTRHa3IwTVpuZjEz - SDZZQnc5a0dGVGEzNXZmNEY5NlZDbVgyNVU0Clo3ZC9MRGp4SHhLUTVCeWlOUUxS - MEtPdW4rUHhjdFB6bFhyUXRQTkRpWjAKLS0tIDVTbWU2V3dJNUZrK1A5U0c5bkc0 - S3VINUJYc3VKcjBZbHVqcGJBSlVPZWcKqPXE01ienWDbTwxo+z4dNAizR3t6uTS+ - KbmSOK1v61Ri0bsM5HItiMP+fE3VCyhqMBmPdcrR92+3oBmiSFnXPA== - -----END AGE ENCRYPTED FILE----- - - recipient: age18jtffqax5v0t6ehh4ypaefl4mfhcrhn6ek3p80mhfp9psx6pd35qew2ww3 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzT3FxcDEzaFRQOVFpNkg2 - Skw4WEIxZzNTWkNBaDRhcUN2ejY4QTAwTERvCkx2clIzT2wyaFJZcjl0RkFXL2p6 - enhqVEZ3ZkNKUU5jTlUxRC9Lb090TzAKLS0tIDBEaG00RFJDZ3ZVVjBGUWJkRHdQ - YkpudG43eURPVWJUejd3Znk5Z29lWlkK0cIngn2qdmiOE5rHOHxTRcjfZYuY3Ej7 - Yy7nYxMwTdYsm/V6Lp2xm8hvSzBEIFL+JXnSTSwSHnCIfgle5BRbug== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2021-11-20T16:21:10Z" - mac: ENC[AES256_GCM,data:5ieT/yv1GZfZFr+OAZ/DBF+6DJHijRXpjNI2kfBun3KxDkyjiu/OFmAbsoVFY/y6YCT3ofl4Vwa56Veo3iYj4njgxyLpLuD1B6zkMaNXaPywbAhuMho7bDGEJZHrlYOUNLdBqW2ytTuFA095IncXE8CFGr38A2hfjcputdHk4R4=,iv:UcBXWtaquflQFNDphZUqahADkeege5OjUY38pLIcFkU=,tag:yy+HSMm+xtX+vHO78nej5w==,type:str] - pgp: [] - unencrypted_suffix: _unencrypted - version: 3.7.1 -``` - -If you add a new host to your `.sops.yaml` file, you will need to update the keys for all secrets that are used by the new host. This can be done like so: -``` -$ nix-shell -p sops --run "sops updatekeys secrets/example.yaml" -``` - -
- -
-5. Deploy - -If you derived your server public key from SSH, all you need in your `configuration.nix` is: - -```nix -{ - # This will add secrets.yml to the nix store - # You can avoid this by adding a string to the full path instead, i.e. - # sops.defaultSopsFile = "/root/.sops/secrets/example.yaml"; - sops.defaultSopsFile = ./secrets/example.yaml; - # This will automatically import SSH keys as age keys - sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; - # This is using an age key that is expected to already be in the filesystem - sops.age.keyFile = "/var/lib/sops-nix/key.txt"; - # This will generate a new key if the key specified above does not exist - sops.age.generateKey = true; - # This is the actual specification of the secrets. - sops.secrets.example-key = {}; - sops.secrets."myservice/my_subdir/my_secret" = {}; -} -``` - -On `nixos-rebuild switch` this will make the keys accessible -via `/run/secrets/example-key` and `/run/secrets/myservice/my_subdir/my_secret`: - -```console -$ cat /run/secrets/example-key -example-value -$ cat /run/secrets/myservice/my_subdir/my_secret -password1 -``` - -`/run/secrets` is a symlink to `/run/secrets.d/{number}`: - -```console -$ ls -la /run/secrets -lrwxrwxrwx 16 root 12 Jul 6:23  /run/secrets -> /run/secrets.d/1 -``` - -
## Set secret permission/owner and allow services to access it diff --git a/docs/HowTo_sshToAge.md b/docs/HowTo_sshToAge.md new file mode 100644 index 0000000..3ee0046 --- /dev/null +++ b/docs/HowTo_sshToAge.md @@ -0,0 +1,198 @@ +# How-To: `sops-nix` with an age key + +This guide assumes you are starting from scratch and are on Linux. + +Nix Darwin users -- check appropriate hidden sections to ensure compatibility. + +If you already know + +## 1. Generate a base key + +This key will be used for you to edit secrets. Be careful not to overwrite an existing key you might be using for other purposes (ex. a github credential). + +```bash +ssh-keygen -t ed25519 # optional: add `-f [path/to/your/key] +``` + +## 2. Derive an age key: + +```bash +# Convert an ssh ed25519 key to an age key +mkdir -p ~/.config/sops/age +nix-shell -p ssh-to-age --run "ssh-to-age \ + -private-key -i ~/.ssh/id_ed25519 \ + > ~/.config/sops/age/keys.txt" +``` + +
+Troubleshooting: encrypted ssh key +If you get the following, +```console +ssh-to-age: failed to parse private ssh key: ssh: this private key is passphrase protected +``` +then your SSH key is encrypted with your password and you will need to create an unencrypted copy temporarily. +```console +$ cp $HOME/.ssh/id_rsa /tmp/id_rsa +$ ssh-keygen -p -N "" -f /tmp/id_rsa +$ nix-shell -p gnupg -p ssh-to-pgp --run "ssh-to-pgp -private-key -i /tmp/id_rsa | gpg --import --quiet" +$ rm /tmp/id_rsa +``` + +
+ +
Troubleshooting: Nix Darwin paths + +When using `nix-darwin` save the `age` key to `$HOME/Library/Application Support/sops/age/keys.txt` or set a [custom](https://github.com/getsops/sops#23encrypting-using-age) configuration directory. + +```bash +# Modified to use stock nix-darwin path +mkdir -p ~/.config/sops/age +nix-shell -p ssh-to-age --run "ssh-to-age \ + -private-key -i ~/.ssh/id_ed25519 \ + > $HOME/Library/Application Support/sops/age/keys.txt" +``` + +
+ + +## 3. Find an `age` public key + +Derive `age` public key directly from the source ssh key: + + +```bash +nix-shell -p ssh-to-age --run "ssh-to-age < ~/.ssh/id_ed25519.pub" +# Expected output: +# age[key....................................................] +``` + +## Create a sops file + +```yaml +keys: + - &server_arpanet age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl +creation_rules: + - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ + key_groups: + - pgp: + age: + - *server_arpanet + - path_regex: secrets/azmidi/[^/]+\.(yaml|json|env|ini)$ + key_groups: + - pgp: + - *admin_alice + - *server_azmidi + age: + - *admin_bob +``` + +## 4. Add your first Secret + +```bash +# In the same directory as your .sops.yaml + +# Passing paths is possible via --config, +# but a path_regex must match relative to your $(pwd) + +# This will open `secrets/example.yml` in your $EDITOR and encrypt the saved file on exit +sops edit secrets/public-ips.yml +``` + +Example yaml: + +```yaml +my-server: + public-ip: + "1.2.3.4" +``` + +## 5. Add secrets to your config + +For the ssh-to-age use case, the config is simple: + +```nix +# /etc/nixos/getSecrets.nix +{ + # This will add secrets/public-ips.yaml to the nix store + # You can avoid this by adding a string to the full path instead, i.e. + # sops.defaultSopsFile = "/root/.sops/secrets/public-ips.yaml"; + sops.defaultSopsFile = ./secrets/public-ips.yaml; + + # This will automatically import SSH keys as age keys + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + + # This is using an age key that is expected to already be in the filesystem + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + + # This will generate a new key if the key specified above does not exist + sops.age.generateKey = true; + + # This is the actual specification of the secrets. + sops.secrets.my-server = {}; + sops.secrets."myservice/my_subdir/my_secret" = {}; +} +``` + +Example usage: + +```nix +# /etc/nixos/staticHosts.nix +{config, ...}:{ + networking.hosts = { + "${config.sops.secrets.ips.my-server.public-ip}" = [ "my.site.org" ]; + }; +} +``` + +Don't forget to include the files above in your config! + +```nix +# /etc/nixos/configuration.nix +{ + imports = [ + ./getSecrets.nix + ./staticHosts.nix + # ... rest of your imports + ] + # ... rest of your config +} +``` + +## 6. Deploy + +Rebuild your system: + +```bash +nixos-rebuild switch # --flake . +``` + +This will make the keys accessible under `/run/secrets/`. + +If you followed this guide exactly, youll find the following files: + +- `/run/secrets/my-server/[secret]` +- `/run/secrets/myservice/my_subdir/[secret]` + +Validate that they are available in plaintext woth a simple `cat` (you might need to prefix `sudo` depending on permissions): + +```bash +cat /run/secrets/my-server +# Expected output: +# example-value + +cat /run/secrets/myservice/my_subdir/my_secret +# Expected output: +# password1 +``` + +`/run/secrets` is a symlink to `/run/secrets.d/{number}`: + +```bash +ls -la /run/secrets +# Expected output: +# lrwxrwxrwx 16 root 12 Jul 6:23  /run/secrets -> /run/secrets.d/1 +``` + +## Further Reading + +If you've made it this far -- check out the [In-Depth Usage Guide](./InDepthUsage.md) for more details. \ No newline at end of file diff --git a/docs/InDepthUsage.md b/docs/InDepthUsage.md new file mode 100644 index 0000000..4d293b5 --- /dev/null +++ b/docs/InDepthUsage.md @@ -0,0 +1,686 @@ +# `sops-nix` In-Depth Usage + +This is a collection of useful walkthroughs in no particular order. Reference the table of contents as you need. + +- [Get a public key from a target machine](#get-a-public-key-from-a-target-machine) + +- [Set secret permission/owner and allow services to access it](#Set-secret-permission-/-owner-and-allow-services-to-access-it) + +- [Restarting/reloading systemd units on secret change](#Restarting-/-reloading-systemd-units-on-secret-change) + +- [Symlinks to other directories](#symlinks-to-other-directories) + +- [Setting a user's password](#setting-a-users-password) + +- [Different file formats](#different-file-formats) + +-[Emit plain file for yaml and json formats](#emit-plain-file-for-yaml-and-json-formats) + +- [Use with home manager](#use-with-home-manager) + +- [Use with GPG instead of SSH keys](#use-with-gpg-instead-of-ssh-keys) + +- [Share secrets between different users](#share-secrets-between-different-users) + +- [Migrate from pass/krops](#migrate-from-passkrops) + +- [Real-world examples](#real-world-examples) + +- [Known limitations](#known-limitations) + +- [Templates](#templates) + +## Get a public key from a target machine + +The easiest way to add new machines is by using SSH host keys (this requires OpenSSH to be enabled). + +If you are using `age`, the `ssh-to-age` tool can be used to convert any SSH Ed25519 public key to the `age` format: +```console +$ nix-shell -p ssh-to-age --run 'ssh-keyscan example.com | ssh-to-age' +age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 +$ nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' +age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 +``` + +For GPG, since sops does not natively support SSH keys yet, sops-nix supports a conversion tool (`ssh-to-pgp`) to store them as GPG keys: + +```console +$ ssh root@server01 "cat /etc/ssh/ssh_host_rsa_key" | nix-shell -p ssh-to-pgp --run "ssh-to-pgp -o server01.asc" +# or with sudo +$ ssh youruser@server01 "sudo cat /etc/ssh/ssh_host_rsa_key" | nix-shell -p ssh-to-pgp --run "ssh-to-pgp -o server01.asc" +0fd60c8c3b664aceb1796ce02b318df330331003 +# or just read them locally/over ssh +$ nix-shell -p ssh-to-pgp --run "ssh-to-pgp -i /etc/ssh/ssh_host_rsa_key -o server01.asc" +0fd60c8c3b664aceb1796ce02b318df330331003 +``` + +The output of these commands is the identifier for the server's key, which can be added to your `.sops.yaml`: + +```yaml +keys: + - &admin_alice 2504791468b153b8a3963cc97ba53d1919c5dfd4 + - &admin_bob age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl + - &server_azmidi 0fd60c8c3b664aceb1796ce02b318df330331003 + - &server_nosaxa age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 +creation_rules: + - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ + key_groups: + - pgp: + - *admin_alice + - *server_azmidi + age: + - *admin_bob + - *server_nosaxa + - path_regex: secrets/azmidi/[^/]+\.(yaml|json|env|ini)$ + key_groups: + - pgp: + - *admin_alice + - *server_azmidi + age: + - *admin_bob +``` + +If you prefer having a separate GPG key, see [Use with GPG instead of SSH keys](#use-with-GPG-instead-of-SSH-keys). + + +## Set secret permission/owner and allow services to access it + +By default secrets are owned by `root:root`. Furthermore +the parent directory `/run/secrets.d` is only owned by +`root` and the `keys` group has read access to it: + +``` console +$ ls -la /run/secrets.d/1 +total 24 +drwxr-x--- 2 root keys 0 Jul 12 6:23 . +drwxr-x--- 3 root keys 0 Jul 12 6:23 .. +-r-------- 1 root root 20 Jul 12 6:23 example-secret +``` + +The secrets option has further parameter to change secret permission. +Consider the following nixos configuration example: + +```nix +{ + # Permission modes are in octal representation (same as chmod), + # the digits represent: user|group|others + # 7 - full (rwx) + # 6 - read and write (rw-) + # 5 - read and execute (r-x) + # 4 - read only (r--) + # 3 - write and execute (-wx) + # 2 - write only (-w-) + # 1 - execute only (--x) + # 0 - none (---) + sops.secrets.example-secret.mode = "0440"; + # Either a user id or group name representation of the secret owner + # It is recommended to get the user name from `config.users.users..name` to avoid misconfiguration + sops.secrets.example-secret.owner = config.users.users.nobody.name; + # Either the group id or group name representation of the secret group + # It is recommended to get the group name from `config.users.users..group` to avoid misconfiguration + sops.secrets.example-secret.group = config.users.users.nobody.group; +} +``` + +
+This example configures secrets for buildkite, a CI agent; +the service needs a token and a SSH private key to function. + +```nix +{ pkgs, config, ... }: +{ + services.buildkite-agents.builder = { + enable = true; + tokenPath = config.sops.secrets.buildkite-token.path; + privateSshKeyPath = config.sops.secrets.buildkite-ssh-key.path; + + runtimePackages = [ + pkgs.gnutar + pkgs.bash + pkgs.nix + pkgs.gzip + pkgs.git + ]; + + }; + + sops.secrets.buildkite-token.owner = config.users.buildkite-agent-builder.name; + sops.secrets.buildkite-ssh-key.owner = config.users.buildkite-agent-builder.name; +} +``` + +
+ +## Restarting/reloading systemd units on secret change + +It is possible to restart or reload units when a secret changes or is newly initialized. + +This behavior can be configured per-secret: +```nix +{ + sops.secrets."home-assistant-secrets.yaml" = { + restartUnits = [ "home-assistant.service" ]; + # there is also `reloadUnits` which acts like a `reloadTrigger` in a NixOS systemd service + }; +} +``` + +## Symlinks to other directories + +Some services might expect files in certain locations. +Using the `path` option a symlink to this directory can +be created: + +```nix +{ + sops.secrets."home-assistant-secrets.yaml" = { + owner = "hass"; + path = "/var/lib/hass/secrets.yaml"; + }; +} +``` + +```console +$ ls -la /var/lib/hass/secrets.yaml +lrwxrwxrwx 1 root root 40 Jul 19 22:36 /var/lib/hass/secrets.yaml -> /run/secrets/home-assistant-secrets.yaml +``` + +## Setting a user's password + +sops-nix has to run after NixOS creates users (in order to specify what users own a secret.) +This means that it's not possible to set `users.users..hashedPasswordFile` to any secrets managed by sops-nix. +To work around this issue, it's possible to set `neededForUsers = true` in a secret. +This will cause the secret to be decrypted to `/run/secrets-for-users` instead of `/run/secrets` before NixOS creates users. +As users are not created yet, it's not possible to set an owner for these secrets. + +The password must be stored as a hash for this to work, which can be created with the command `mkpasswd` +```console +$ echo "password" | mkpasswd -s +$y$j9T$WFoiErKnEnMcGq0ruQK4K.$4nJAY3LBeBsZBTYSkdTOejKU6KlDmhnfUV3Ll1K/1b. +``` + +```nix +{ config, ... }: { + sops.secrets.my-password.neededForUsers = true; + + users.users.mic92 = { + isNormalUser = true; + hashedPasswordFile = config.sops.secrets.my-password.path; + }; +} +``` + +**Note:** If you are using Impermanence, the key used for secret decryption (`sops.age.keyFile`, or the host SSH keys) must be in a persisted directory, +loaded early enough during boot. For example: + +```nix +sops.age.keyFile = "/nix/persist/var/lib/sops-nix/key.txt"; +``` + +or: + +```nix +fileSystems."/etc/ssh".neededForBoot = true; +``` + +## Different file formats + +At the moment we support the following file formats: YAML, JSON, INI, dotenv and binary. + +sops-nix allows specifying multiple sops files in different file formats: + +```nix +{ + imports = [ ]; + # The default sops file used for all secrets can be controlled using `sops.defaultSopsFile` + sops.defaultSopsFile = ./secrets.yaml; + # If you use something different from YAML, you can also specify it here: + #sops.defaultSopsFormat = "yaml"; + sops.secrets.github_token = { + # The sops file can be also overwritten per secret... + sopsFile = ./other-secrets.json; + # ... as well as the format + format = "json"; + }; +} +``` + +### YAML + +Open a new file with sops ending in `.yaml`: + +```console +$ sops secrets.yaml +``` + +Then, put in the following content: + +```yaml +github_token: 4a6c73f74928a9c4c4bc47379256b72e598e2bd3 +ssh_key: | + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACDENhLwQI4v/Ecv65iCMZ7aZAL+Sdc0Cqyjkd012XwJzQAAAJht4at6beGr + egAAAAtzc2gtZWQyNTUxOQAAACDENhLwQI4v/Ecv65iCMZ7aZAL+Sdc0Cqyjkd012XwJzQ + AAAEBizgX7v+VMZeiCtWRjpl95dxqBWUkbrPsUSYF3DGV0rsQ2EvBAji/8Ry/rmIIxntpk + Av5J1zQKrKOR3TXZfAnNAAAAE2pvZXJnQHR1cmluZ21hY2hpbmUBAg== + -----END OPENSSH PRIVATE KEY----- +``` + +You can include it like this in your `configuration.nix`: + +```nix +{ + sops.defaultSopsFile = ./secrets.yaml; + # YAML is the default + #sops.defaultSopsFormat = "yaml"; + sops.secrets.github_token = { + format = "yaml"; + # can be also set per secret + sopsFile = ./secrets.yaml; + }; +} +``` + +### JSON + +Open a new file with sops ending in `.json`: + +```console +$ sops secrets.json +``` + +Then, put in the following content: + +``` json +{ + "github_token": "4a6c73f74928a9c4c4bc47379256b72e598e2bd3", + "ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\\nQyNTUxOQAAACDENhLwQI4v/Ecv65iCMZ7aZAL+Sdc0Cqyjkd012XwJzQAAAJht4at6beGr\\negAAAAtzc2gtZWQyNTUxOQAAACDENhLwQI4v/Ecv65iCMZ7aZAL+Sdc0Cqyjkd012XwJzQ\\nAAAEBizgX7v+VMZeiCtWRjpl95dxqBWUkbrPsUSYF3DGV0rsQ2EvBAji/8Ry/rmIIxntpk\\nAv5J1zQKrKOR3TXZfAnNAAAAE2pvZXJnQHR1cmluZ21hY2hpbmUBAg==\\n-----END OPENSSH PRIVATE KEY-----\\n" +} +``` + +You can include it like this in your `configuration.nix`: + +```nix +{ + sops.defaultSopsFile = ./secrets.json; + # YAML is the default + sops.defaultSopsFormat = "json"; + sops.secrets.github_token = { + format = "json"; + # can be also set per secret + sopsFile = ./secrets.json; + }; +} +``` + +### Binary + +This format allows to encrypt an arbitrary binary format that can't be put into +JSON/YAML files. Unlike the other two formats, for binary files, one file corresponds to one secret. + +To encrypt an binary file use the following command: + +``` console +$ sops -e /etc/krb5/krb5.keytab > krb5.keytab +# an example of what this might result in: +$ head krb5.keytab +{ + "data": "ENC[AES256_GCM,data:bIsPHrjrl9wxvKMcQzaAbS3RXCI2h8spw2Ee+KYUTsuousUBU6OMIdyY0wqrX3eh/1BUtl8H9EZciCTW29JfEJKfi3ackGufBH+0wp6vLg7r,iv:TlKiOmQUeH3+NEdDUMImg1XuXg/Tv9L6TmPQrraPlCQ=,tag:dVeVvRM567NszsXKK9pZvg==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "lastmodified": "2020-07-06T06:21:06Z", + "mac": "ENC[AES256_GCM,data:ISjUzaw/5mNiwypmUrOk2DAZnlkbnhURHmTTYA3705NmRsSyUh1PyQvCuwglmaHscwl4GrsnIz4rglvwx1zYa+UUwanR0+VeBqntHwzSNiWhh7qMAQwdUXmdCNiOyeGy6jcSDsXUeQmyIWH6yibr7hhzoQFkZEB7Wbvcw6Sossk=,iv:UilxNvfHN6WkEvfY8ZIJCWijSSpLk7fqSCWh6n8+7lk=,tag:HUTgyL01qfVTCNWCTBfqXw==,type:str]", + "pgp": [ + { + +``` + +It can be decrypted again like this: + +``` console +$ sops -d krb5.keytab > /tmp/krb5.keytab +``` + +This is how it can be included in your `configuration.nix`: + +```nix +{ + sops.secrets.krb5-keytab = { + format = "binary"; + sopsFile = ./krb5.keytab; + }; +} +``` + +## Emit plain file for yaml and json formats + +By default, sops-nix extracts a single key from yaml and json files. If you +need the plain file instead of extracting a specific key from the input document, +you can set `key` to an empty string. + +For example, the input document `my-config.yaml` likes this: + +```yaml +my-secret1: ENC[AES256_GCM,data:tkyQPQODC3g=,iv:yHliT2FJ74EtnLIeeQtGbOoqVZnF0q5HiXYMJxYx6HE=,tag:EW5LV4kG4lcENaN2HIFiow==,type:str] +my-secret2: ENC[AES256_GCM,data:tkyQPQODC3g=,iv:yHliT2FJ74EtnLIeeQtGbOoqVZnF0q5HiXYMJxYx6HE=,tag:EW5LV4kG4lcENaN2HIFiow==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] +... +``` + +This is how it can be included in your NixOS module: + +```nix +{ + sops.secrets.my-config = { + format = "yaml"; + sopsFile = ./my-config.yaml; + key = ""; + }; +} +``` + +Then, it will be mounted as `/run/secrets/my-config`: + +```yaml +my-secret1: hello +my-secret2: hello +``` + +## Use with home manager + +sops-nix also provides a home-manager module. +This module provides a subset of features provided by the system-wide sops-nix since features like the creation of the ramfs and changing the owner of the secrets are not available for non-root users. + +The home-manager module requires systemd/user as it runs a service called `sops-nix.service` rather than an activation script. +While the sops-nix _system_ module decrypts secrets to the system non-persistent `/run/secrets`, the _home-manager_ module places them in the users non-persistent `$XDG_RUNTIME_DIR/secrets.d`. +Additionally secrets are symlinked to the users home at `$HOME/.config/sops-nix/secrets` which are referenced for the `.path` value in sops-nix. +This requires that the home-manager option `home.homeDirectory` is set to determine the home-directory on evaluation. It will have to be manually set if home-manager is configured as stand-alone or on non NixOS systems. + +Depending on whether you use home-manager system-wide or stand-alone using a home.nix, you have to import it in a different way. +This example shows the `flake` approach from the recommended example [Install: Flakes (current recommendation)](#Flakes (current recommendation)) + +```nix +{ + # NixOS system-wide home-manager configuration + home-manager.sharedModules = [ + inputs.sops-nix.homeManagerModules.sops + ]; +} +``` + +```nix +{ + # Configuration via home.nix + imports = [ + inputs.sops-nix.homeManagerModules.sops + ]; +} +``` + +This example show the `channel` approach from the example [Install: nix-channel](#nix-channel). All other methods work as well. + +```nix +{ + # NixOS system-wide home-manager configuration + home-manager.sharedModules = [ + + ]; +} +``` + +```nix +{ + # Configuration via home.nix + imports = [ + + ]; +} +``` + +The actual sops configuration is in the `sops` namespace in your home.nix (or in the `home-manager.users.` namespace when using home-manager system-wide): +```nix +{ + sops = { + age.keyFile = "/home/user/.age-key.txt"; # must have no password! + # It's also possible to use a ssh key, but only when it has no password: + #age.sshKeyPaths = [ "/home/user/path-to-ssh-key" ]; + defaultSopsFile = ./secrets.yaml; + secrets.test = { + # sopsFile = ./secrets.yml.enc; # optionally define per-secret files + + # %r gets replaced with a runtime directory, use %% to specify a '%' + # sign. Runtime dir is $XDG_RUNTIME_DIR on linux and $(getconf + # DARWIN_USER_TEMP_DIR) on darwin. + path = "%r/test.txt"; + }; + }; +} +``` + +The secrets are decrypted in a systemd user service called `sops-nix`, so other services needing secrets must order after it: +```nix +{ + systemd.user.services.mbsync.Unit.After = [ "sops-nix.service" ]; +} +``` + +### Qubes Split GPG support + +If you are using Qubes with the [Split GPG](https://www.qubes-os.org/doc/split-gpg), +then you can configure sops to utilize the `qubes-gpg-client-wrapper` with the `sops.gnupg.qubes-split-gpg` options. +The example above updated looks like this: +```nix +{ + sops = { + gnupg.qubes-split-gpg = { + enable = true; + domain = "vault-gpg"; + }; + defaultSopsFile = ./secrets.yaml; + secrets.test = { + # sopsFile = ./secrets.yml.enc; # optionally define per-secret files + + # %r gets replaced with a runtime directory, use %% to specify a '%' + # sign. Runtime dir is $XDG_RUNTIME_DIR on linux and $(getconf + # DARWIN_USER_TEMP_DIR) on darwin. + path = "%r/test.txt"; + }; + }; +} +``` + +## Use with GPG instead of SSH keys + +If you prefer having a separate GPG key, sops-nix also comes with a helper tool, `sops-init-gpg-key`: + +```console +$ nix run github:Mic92/sops-nix#sops-init-gpg-key -- --hostname server01 --gpghome /tmp/newkey +# You can use the following command to save it to a file: +$ cat > server01.asc < server01.asc <