Need help with sops-nix?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

156 Stars 22 Forks MIT License 170 Commits 16 Opened issues


Atomic secret provisioning for NixOS based on sops

Services available


Need anything else?

Contributors list


Atomic secret provisioning for NixOS based on sops.

How it works

Sops-nix decrypts secrets sops files on the target machine to files specified in the NixOS configuration at activation time. It also adjusts file permissions/owner/group. It uses either host ssh keys or GPG keys for decryption. In future we will also support cloud key management APIs such as AWS KMS, GCP KMS, Azure Key Vault or Hashicorp's vault.


  • Compatible with all NixOS deployment frameworks: NixOps, nixos-rebuild, krops, morph, nixus
  • Version-control friendly: Since all files are encrypted they can directly committed to version control. The format is readable in diffs and there are also ways of showing git diffs in cleartext
  • Works well in teams: sops-nix comes with nix-shell hooks that allows quickly import multiple people to import all used keys. The cryptography used in sops is designed to be scalable: Secrets are only encrypted once with a master key instead of each machine/developer key.
  • CI friendly: Since sops files can be added to the Nix store without leaking secrets, a machine definition can be built as a whole.
  • Atomic upgrades: New secrets are written to a new directory which replaces the old directory in an atomic step.
  • Rollback support: If sops files are added to Nix store, old secrets can be rolled back. This is optional.
  • Fast: Unlike solutions implemented by NixOps, krops and morph there is no extra step required to upload secrets
  • Different storage formats: Secrets can be stored in YAML, JSON or binary.
  • Minimize configuration errors: sops files are checked against the configuration at evaluation time.


There is a configuration.nix example in the deployment step of our usage example.

Usage example

1. Install nix-sops

Choose one of the following methods:

niv (Current recommendation)

First add it to niv:

$ niv add Mic92/sops-nix

Then add the following to your configuration.nix in the

  imports = [ "${(import ./nix/sources.nix).sops-nix}/modules/sops" ];


As root run:

$ nix-channel --add sops-nix
$ nix-channel --update

Then add the following to your configuration.nix in the

  imports = [  ];


Add the following to your configuration.nix:

  imports = [ "${builtins.fetchTarball ""}/modules/sops" ];

or with pinning:

  imports = let
    # replace this with an actual commit id or tag
    commit = "298b235f664f925b433614dc33380f0662adfc3f";
  in [ 
    "${builtins.fetchTarball {
      url = "${commit}.tar.gz";
      # replace this with an actual hash
      sha256 = "0000000000000000000000000000000000000000000000000000";


If you use experimental nix flakes support:

  inputs.sops-nix.url = github:Mic92/sops-nix;
  # optional, not necessary for the module
  #inputs.sops-nix.inputs.nixpkgs.follows = "nixpkgs";

outputs = { self, nixpkgs, sops-nix }: { # change yourhostname to your actual hostname nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem { # change to your system: system = "x86_64-linux"; modules = [ ./configuration.nix sops-nix.nixosModules.sops ]; }; }; }

2. Generate a GPG key for yourself

First generate yourself a GPG key or use nix-sops conversion tool to convert an existing ssh key (we only support RSA keys right now):

$ nix run -f ssh-to-pgp
$ ssh-to-pgp -private-key -i $HOME/.ssh/id_rsa | gpg --import --quiet
# This exports the public key
$ ssh-to-pgp -i $HOME/.ssh/id_rsa -o $USER.asc

If you get:

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 need to create an unencrypted copy temporarily:

$ cp $HOME/.ssh/id_rsa /tmp/id_rsa
$ ssh-keygen -p -N "" -f /tmp/id_rsa
$ ssh-to-pgp -private-key -i /tmp/id_rsa | gpg --import --quiet

The hex string printed here is your GPG fingerprint that can be exported to

export SOPS_PGP_FP=2504791468b153b8a3963cc97ba53d1919c5dfd4

If you have generated a GnuPG key directly you can get your fingerprint like this:

gpg --list-secret-keys
sec   rsa2048 1970-01-01 [SCE]
uid           [ unknown] root 

The fingerprint here is


3. Get a PGP Public key for your machine

The easiest way to add new hosts is using ssh host keys (requires openssh to be enabled). Since sops does not natively supports ssh keys yet, nix-sops supports a conversion tool to store them as gpg keys.

$ nix-shell -p ssh-to-pgp
$ ssh [email protected] "cat /etc/ssh/ssh_host_rsa_key" | ssh-to-pgp -o server01.asc
# or with sudo
$ ssh [email protected] "sudo cat /etc/ssh/ssh_host_rsa_key" | ssh-to-pgp -o server01.asc
# Or just read them locally (or in a ssh session)
$ ssh-to-pgp -i /etc/ssh/ssh_host_rsa_key -o server01.asc

Also the hex string here is the fingerprint of your server's gpg key that can be exported append to

export SOPS_PGP_FP=${SOPS_PGP_FP}:2504791468b153b8a3963cc97ba53d1919c5dfd4

If you prefer having a separate GnuPG key, see Use with GnuPG instead of ssh keys.

4. Create a sops file

To create a sops file you need to set export

to include both the fingerprint of your personal gpg key (and your colleagues) and your servers:
export SOPS_PGP_FP="2504791468b153b8a3963cc97ba53d1919c5dfd4,2504791468b153b8a3963cc97ba53d1919c5dfd4"

sops-nix automates that with a hook for nix-shell and also takes care of importing all keys, allowing public keys to be stored in git:

# shell.nix
with import  {};
mkShell {
  # imports all files ending in .asc/.gpg and sets $SOPS_PGP_FP.
  sopsPGPKeyDirs = [ 
  # Also single files can be imported.
  #sopsPGPKeys = [ 
  #  "./keys/users/mic92.asc"
  #  "./keys/hosts/server01.asc"
  nativeBuildInputs = [
    (pkgs.callPackage  {}).sops-pgp-hook

Our directory structure looks like this:

$ tree .
├── keys
│   ├── hosts
│   │   └── server01.asc
│   └── users
│       └── mic92.asc

After that you can open a new file with sops

nix-shell --run "sops secrets.yaml"

This will start your configured editor In our example we put the following content in it:

example-key: example-value

NOTE: At the moment we do not support nested data structures that sops support. This might change in the future. See also Different file formats

As a result when saving the file the following content will be in it:

example-key: ENC[AES256_GCM,data:7QIOMLd2kZkeVVpH0Q==,iv:ROh+J59ZM6BtjZLhRj1Ylk6ROEvsiX6/UR8obHX8YcQ=,tag:QOiFoHKyGFBkhr9lcWBB3Q==,type:str]
    kms: []
    gcp_kms: []
    azure_kv: []
    lastmodified: '2020-07-13T09:09:14Z'
    mac: ENC[AES256_GCM,data:BCwTBxaW6qINVfixC32EEYrlqPvGz47wF+o/vNPqcwed1HPwZezlNy7Z4NFLbRcCLAELyeMqkJ+fi9XCWvnT3UvfwB45COpz/xZphURt3gyCVOyd9mT/s9cJ1O9vNy5iKblqCae2X0CTKee/GxJ0G725LDOL4r+oHM1+WWEInWo=,iv:S43qegidSqcaUaDjvQpEQj/qvF/OZcW32Yo05CfyTUs=,tag:npj5auJXZrg7jQwYSjC6Vg==,type:str]
    -   created_at: '2020-07-13T08:34:30Z'
        enc: |
            -----BEGIN PGP MESSAGE-----

        -----END PGP MESSAGE-----
    fp: 0FD60C8C3B664ACEB1796CE02B318DF330331003
-   created_at: '2020-07-13T08:34:30Z'
    enc: |
        -----BEGIN PGP MESSAGE-----

        -----END PGP MESSAGE-----
    fp: 0FD60C8C3B664ACEB1796CE02B318DF330331003
-   created_at: '2020-07-13T08:34:30Z'
    enc: |
        -----BEGIN PGP MESSAGE-----

        -----END PGP MESSAGE-----
    fp: 9F89C5F69A10281A835014B09C3DC61F752087EF
unencrypted_suffix: _unencrypted
version: 3.5.0

5. Deploy

If you derived your server public key from ssh, all you need in your configuration.nix is:

  imports = [  ];
  # 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.yaml";
  sops.defaultSopsFile = ./secrets.yaml;
  sops.secrets.example-key = {};


nixos-rebuild switch
this will make the key accessible via
$ cat /run/secrets/example-key

is a symlink to
$ 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

By default secrets are owned by

. Furthermore the parent directory
is only owned by
and the
group has read access to it:
$ ls -la /run/secrets.d/1
total 24
drwxr-x--- 2 root keys   0 Jul 18 15:35 .
drwxr-x--- 3 root keys   0 Jul 18 15:35 ..
-r-------- 1 root root  20 Jul 18 15:35 borgbackup

The secrets option has further parameter to change secret permission. Consider the following nixos configuration example:

  # Permission modes are in octal representation,
  # the digits reprsent: user|group|owner
  # 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 `` to avoid misconfiguration
  sops.secrets.example-secret.owner =;
  # Either the group id or group name representation of the secret group
  # It is recommended to get the group name from `` to avoid misconfiguration =;

To access secrets each non-root process/service needs to be part of the keys group. For systemd services this can be achieved as following:

{ = {
    serviceConfig.SupplementaryGroups = [ ];

For login or system users this can be done like this:

  users.users.example-user.extraGroups = [ ];

The following example configures secrets for buildkite, a CI agent the service needs a token and a ssh private key to function:

{ pkgs, config, ... }:
  services.buildkite-agents.builder = {
    enable = true;
    tokenPath = config.sops.secrets.buildkite-token.path;
    privateSshKeyPath = config.sops.secrets.buildkite-ssh-key.path;

runtimePackages = [

}; = { serviceConfig.SupplementaryGroups = [ ]; };

sops.secrets.buildkite-token.owner =; sops.secrets.buildkite-ssh-key.owner =; }

Symlinks to other directories

Some services might expect files in certain locations. Using the

option as symlink to this directory can be created:
  sops.secrets."home-assistant-secrets.yaml" = {
    owner = "hass";
    path = "/var/lib/hass/secrets.yaml";
$ 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

Different file formats

At the moment we support the following file formats: YAML, JSON, binary

NOTE: At the moment we do not support nested data structures that sops support. This might change in the future:

We support the following yaml:

key: 1

but not:

  key: 1

nix-sops allows to specify multiple sops files in different file formats:

  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";


Open a new file with sops ending in

$ sops secrets.yaml

Than put in the following content:

github_token: 4a6c73f74928a9c4c4bc47379256b72e598e2bd3
# multi-line strings in yaml start with an |
ssh_key: |

You can include it like this in your

  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;


Open a new file with sops ending in

$ sops secrets.json

Than put in the following content:

  "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

  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;


Unlike the other two formats for binaries one file correspond to one secret. This format allows to encrypt an arbitrary binary format that can't be put into JSON/YAML files.

To encrypt an binary file use the following command:

$ sops -e /tmp/krb5.keytab > krb5.keytab
$ 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:

$ sops -d krb5.keytab > /tmp/krb5.keytab

This is how it can be included in your configuration.nix:

  sops.secrets.krb5-keytab = {
    format = "binary";
    sopsFile = ./krb5.keytab;

Use with GnuPG instead of ssh keys

If you prefer having a separate GnuPG key, sops-nix also comes with a helper tool:

$ nix-shell -p sops-init-gpg-key
$ sops-init-gpg-key --hostname server01 --gpghome /tmp/newkey
You can use the following command to save it to a file:
cat > server01.asc <

In this case you need to make upload the gpg key directory

to your server. If you uploaded it to
than your sops configuration will look like this:
  # Make sure that `/var/lib/sops` is owned by root and is not world-readable/writable
  sops.gnupgHome = "/var/lib/sops";
  # disable import host ssh keys
  sops.sshKeyPaths = [];

However be aware that this will also run gnupg on your server including the gnupg daemon. Gnupg is in general not great software and might break in hilarious ways. If you experience problems, you are on your own. If you want a more stable and predictable solution go with ssh keys or one of the KMS services.

Share secrets between different users

Secrets can be shared between different users by creating different files pointing to the same sops key but with different permissions. In the following example the

secret is exposed as
and as
  sops.secrets.drone-server = {
    owner =;
    key = "drone";
  sops.secrets.drone-agent = {
    owner =;
    key = "drone";

Migrate from pass/krops

If you have used pass before i.e. in krops than you can use the following one-liner to convert all your secrets to a yaml structure.

$ for i in *.gpg; do echo "$(basename $i .gpg): |\n$(pass $(dirname $i)/$(basename $i .gpg)| sed 's/^/  /')"; done

Copy the output to the editor you have opened with sops.

Realworld Examples

My personal configuration makes extensive usage of sops-nix. Each host has a secrets directory containing secrets for the host.

Known limitations

Restarting systemd services

Right now systemd services are not restarted automatically. We want to implement this in future.

Initrd secrets

sops-nix does not fully support initrd secrets. This is because

nixos-rebuild switch
installs the bootloader before running sops-nix activation hook. At the moment it is be possible to run
nixos-rebuild test
nixos-rebuild switch
to provision initrd secrets key before the initrd secrets are built. In future we hope to extend nixos to allow keys to be provisioned in the bootloader install phase.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.