# sops-nix ![sops-nix logo](https://github.com/Mic92/sops-nix/releases/download/assets/logo.gif "Logo of sops-nix") Atomic, declarative, and reproducible secret provisioning for NixOS based on [sops](https://github.com/mozilla/sops). ## How it works Secrets are decrypted from [`sops` files](https://github.com/mozilla/sops#2usage) during activation time. The secrets are stored as one secret per file and access-controlled by full declarative configuration of their users, permissions, and groups. GPG keys or `age` keys can be used for decryption, and compatibility shims are supported to enable the use of SSH RSA or SSH Ed25519 keys. Sops also supports cloud key management APIs such as AWS KMS, GCP KMS, Azure Key Vault and Hashicorp Vault. While not officially supported by sops-nix yet, these can be controlled using environment variables that can be passed to sops. ## Features - Compatible with all NixOS deployment frameworks: [NixOps](https://github.com/NixOS/nixops), nixos-rebuild, [krops](https://github.com/krebs/krops/), [morph](https://github.com/DBCDK/morph), [nixus](https://github.com/Infinisil/nixus), etc. - Version-control friendly: Since all files are encrypted they can be directly committed to version control without worry. Diffs of the secrets are readable, and [can be shown in cleartext](https://github.com/mozilla/sops#showing-diffs-in-cleartext-in-git). - CI friendly: Since sops files can be added to the Nix store without leaking secrets, a machine definition can be built as a whole from a repository, without needing to rely on external secrets or services. - Home-manager friendly: Provides a home-manager module - Works well in teams: sops-nix comes with `nix-shell` hooks that allows multiple people to quickly import all GPG keys. The cryptography used in sops is designed to be scalable: Secrets are only encrypted once with a master key instead of encrypted per machine/developer key. - Atomic upgrades: New secrets are written to a new directory which replaces the old directory atomically. - Rollback support: If sops files are added to the Nix store, old secrets can be rolled back. This is optional. - Fast time-to-deploy: Unlike solutions implemented by NixOps, krops and morph, no extra steps are required to upload secrets. - A variety of storage formats: Secrets can be stored in YAML, dotenv, INI, JSON or binary. - Minimizes configuration errors: sops files are checked against the configuration at evaluation time. ## Demo There is a `configuration.nix` example in the [deployment step](#deploy-example) of our usage example. ## Installation 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). ### Flakes (current recommendation) If you use experimental nix flakes support: ``` nix { inputs.sops-nix.url = "github:Mic92/sops-nix"; inputs.sops-nix.inputs.nixpkgs.follows = "nixpkgs"; outputs = { self, nixpkgs, sops-nix }: { # change `yourhostname` to your actual hostname nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem { # customize to your system system = "x86_64-linux"; modules = [ ./configuration.nix sops-nix.nixosModules.sops ]; }; }; } ``` ### Alternative Installs
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 { inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; inputs.nix-darwin.url = "github:nix-darwin/nix-darwin/master"; inputs.nix-darwin.inputs.nixpkgs.follows = "nixpkgs"; inputs.sops-nix.url = "github:Mic92/sops-nix"; #inputs.sops-nix.inputs.nixpkgs.follows = "nixpkgs"; outputs = { self, nix-darwin, nixpkgs, sops-nix }: { darwinConfigurations.yourhostname = nix-darwin.lib.darwinSystem { modules = [ ./configuration.nix sops-nix.darwinModules.sops ]; }; }; } ```
Niv (recommended if not using flakes) See [`niv`](https://github.com/nmattia/niv) (recommended if not using flakes) First add it to niv: ```console $ niv add Mic92/sops-nix ``` Then add the following to your `configuration.nix` in the `imports` list: ```nix { imports = [ "${(import ./nix/sources.nix).sops-nix}/modules/sops" ]; } ```
fetchTarball (None of the above) Add the following to your `configuration.nix`: ```nix { imports = let # replace this with an actual commit id or tag commit = "298b235f664f925b433614dc33380f0662adfc3f"; in [ "${builtins.fetchTarball { url = "https://github.com/Mic92/sops-nix/archive/${commit}.tar.gz"; # replace this with an actual hash sha256 = "0000000000000000000000000000000000000000000000000000"; }}/modules/sops" ]; } ```
## Usage `sops-nix` supports two basic ways of encryption, GPG and `age`. First, pick your track. If you have time, there are some excellent resources on each key type that we won't cover here. 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) ```mermaid --- config: look: classic theme: dark layout: dagre --- graph TD; Start[Start] ageLink[Age How-To] click ageLink href "./docs/HowTo_age.md" sshLink[SSH How-To] click ageLink href "./docs/HowTo_ssh.md" gpgLink[GPG How-To] click ageLink href "./docs/HowTo_gpg.md" Decision1{I have opinions on key types} Start --> Decision1 Decision1 -->|No| Decision2{I already use SSH} Decision1 -->|Yes| Know{Click a tutorial} Know -->|Age| ageLink Know -->|SSH| sshLink Know -->|GPG| gpgLink Decision2 -->|Yes| sshLink Decision2 -->|No| ageLink ``` ## 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 <