Sopsidy is an extension for sops-nix that automates the creation of sops files based on configured collection scripts per secret. These scripts typically collect secrets from password managers like Bitwarden or 1Password.
Sopsidy provides two main exports: a NixOS module and a
buildSecretsCollector
function to build a single secrets
collector for a collection of hosts.
The NixOS module adds a new section of options:
sops.secrets.*.collect
. Docs for these options can be
found in docs/flake-module.md.
The main option is sops.secrets.*.collect.script
that
must be set for every secret defining how to find the secret.
There are additional plugins (only the rbw one so far) that
can add more options and set the script for you. For example,
the rbw plugin adds a collect.id
option and will set the
script to get the password of the entry with the id in your
bitwarden vault, provided rbw has been setup correctly.
Then the buildSecretsCollector
function will use all the
collect.script
settings for all secrets across all hosts and
create the secrets collector script. Running this script in the
repository will run all the collect scripts and fill their outputs
into a json blob which is passed to sops along with the related
host's age pubkeys to encrypt all the repository's sops files.
The NixOS module also adds the global option sops.hostPubKey
which allows the secrets collector to manage the age keys for
each sops file. This removes the need for the sops config file,
.sops.yaml
, and it should actually be removed because it
interferes with the secret collector script.
Sopsidy is designed to completely take over the sops files
in the repository, so if sops-nix is already being used
be sure to have backups of all existing sops files before
setting up sopsidy. Also as mentioned before, move .sops.yaml
out of the repository as it interferes with sopsidy's native
management of age keys for each sops file -
based on the sops.hostPubKey
option provided by the sopsidy
nixos module.
First import both the sops-nix and sopsdiy NixOS modules
in the NixOS system. Then use the sopsidy tool
buildSecretsCollector
to build the collect-secrets
script for all the hosts. The script should then
be exported as a flake output or included in a devshell.
A flake parts module is available to set up the script
and make it available to be exported or added to a devshell.
Documentation for the flake-parts module options can be found
at docs/flake-module.md
Below are examples for setting up sopsidy with and without flake-parts. Usage without flakes is not currently supported.
With flake-parts (recommended method)
{
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
sops-nix.url = "github:Mic92/sops-nix";
sopsidy.url = "github:timewave-computer/sopsidy";
sopsidy.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{self, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.sopsidy.flakeModule
];
perSystem = { config, ... }: {
sopsidy.hosts = self.nixosConfigurations;
packages.collect-secrets = config.sopsidy.package;
};
flake.nixosConfigurations = {
nixos = nixpkgs.lib.nixosSystem {
modules = [
inputs.sops-nix.nixosModules.default
inputs.sopsidy.nixosModules.default
./configuration.nix
];
};
};
};
}
Without flake-parts
{
inputs = {
sops-nix.url = "github:Mic92/sops-nix";
sopsidy.url = "github:timewave-computer/sopsidy";
sopsidy.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{self, nixpkgs, ...}:
let pkgs = import nixpkgs { system = "x86_64-linux"; }; in
{
nixosConfigurations = {
nixos = nixpkgs.lib.nixosSystem {
modules = [
inputs.sops-nix.nixosModules.default
inputs.sopsidy.nixosModules.default
./configuration.nix
];
};
};
packages.x86_64-linux.collect-secrets = sopsidy.lib.buildSecretsCollector {
inherit pkgs;
hosts = self.nixosConfigurations;
};
};
}
Once the NixOS module as been imported and the collect-secrets
package has been setup, set the sops.hostPubKey
with the host's age
pubkey so that the secret collector (collect-secrets
script) will
know what age key to pass to sops.
For remote systems this can be found with:
nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 <server-domain> | tail -n 1 | ssh-to-age'
If you are already on the system, it can be found with:
nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
Then in the host's configuration.nix
set the required sops-nix and sopsidy settings.
{
sops.hostPubKey = "<age pubkey>";
sops.defaultSopsFile = ./secrets.yaml;
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
}
Now secrets can be added for any services.
All thats needed is to define the secret in a NixOS module
just like with sops-nix except with an additional collect
section.
{
sops.secrets."service/admin-password" = {
collect.script = ''
rbw get <id>
'';
};
}
Using the rbw plugin (which is included by default), this is equivalent to:
{
sops.secrets."service/admin-password" = {
collect = {
rbw.id = "<id>";
};
};
}
To then create the sops file secrets.yaml
run:
nix run .#collect-secrets
Confirm that secrets.yaml
has a service.admin-password
entry.
Then the host can be deployed with the secret.
Most of the work is done within sops and sops-nix.
The secret collector script passes all secrets through pipes,
so sensitive data is never exposed into environment variables
or process IDs.
It is of course possible for the collect scripts themselves to
leak sensitive data. Avoid using command subsitution
($(command outputting secret)
) within collect scripts because
that data can be found by watching process ids.
The script will encrypt each sops file with only the age keys
of hosts that have secrets defined for that file.
If there are secrets that shouldn't be shared between hosts, make
sure to use the sops.secrets.*.sopsFile
from sops-nix to use
different sops files for groups of secrets.
Sopsidy is just a wrapper around sops-nix and sops, so most of the credit goes to those two projects for doing the heavy lifting.
The inspiration for this project comes from opsops. Opsops requires creating a separate yaml file describing how secrets are collected. Sopsidy lets you instead put the collection scipts alongside the NixOS sops definitions for them.
agenix-rekey was
also helpful for both inspiration and an example of how to inject
new options into attrsOf submodule
types.