baldino.dev > Blog

secs-man: a secrets manager you can (not) rely on

4 min read

I've been writing secs-man, a SECretS MANager tool for managing, backing up and deploying secret files (SSH keys, WireGuard keys, ...). secs-man can be used to export/import encrypted backups of your secret files on local storage (I personally use a dedicated flashdrive).

Philosophy

I designed secs-man around the following philosophy: any tool that sits between you and your critical backups should be replaceable, and you should never depend exclusively on the availability of the tool. For this reason, every behaviour of secs-man is designed to be fully reproducible by hand relying only on established tools, such as the coreutils tools and age. Clear instructions on how to reproduce its behaviour with these tools are given in the README.md.

Usage

The detailed usage of the tool is given in the README.md, but in a nutshell: secs-man is designed to work with a centralized "secrets" directory containing all the necessary files. In my case I use /secrets, owned by root (except the SSH key files, which are owned by my non-root user). All of this is configured inside the configuration file and handled automatically by secs-man for a set-it-and-forget-it workflow. Then, where possible, the secret is used in-place from its path (eg: /secrets/wg/wg0.private). When this is not possible (such as for SSH keys) I use a symlink from the correct file placement to the path in the centralized directory. This plays nicely with a NixOS setup, as shown below.

secs-man can also be used to manage secrets of a remote machine using the available secs-man-ssh script. The script assumes accessibility to the remote machine with ssh keys but does not assume root-login through SSH, as is usual for hardened SSH on remote machines.

Usage with NixOS

I designed secs-man to work nicely with NixOS. Having a centralized secrets directory is sometimes problematic or a bit of an annoyance, for example having to remember to create the appropriate symlinks. This problem is cleanly solved by NixOS, where you can declaratively set the correct filepaths or symlinks and have them activated automatically on each install.

For example, in my configuration I have the following instruction to automate the symlinks for SSH keys

nix
# the correct ownership of the secrets is handled by the secrets manifest
systemd.tmpfiles.rules =
  let
    <my-user> = config.users.users.<my-user>;
  in
  [
    "d ${<my-user>.home}/.ssh 0700 ${<my-user>.name} ${<my-user>.group} -"
    "L ${<my-user>.home}/.ssh/id_ed25519 - - - - /secrets/ssh/id_ed25519"
    "L ${<my-user>.home}/.ssh/id_ed25519.pub - - - - /secrets/ssh/id_ed25519.pub"
  ];

and here's the configuration for WireGuard accessing the secrets from the centralized directory

nix
networking.wg-quick.interfaces = {
  wg0 = {
    privateKeyFile = "/secrets/wg/wg0.private";

    [...]
  };
};

This approach is also a great fit for NixOS deployment through nixos-anywhere: by using nixos-anywhere's --extra-files flag, the centralized secrets directory can be automatically imported during the install phase, removing the need for an extra imperative phase for deploying the secrets.

Why not ___?

There exist some battle-tested solutions for managing secrets that mesh well with NixOS, such as sops-nix and agenix, so why not use those?

The answer can be classified as "good security practices" or "paranoia" depending on who you ask, and it boils down to the fact that I don't want my (albeit encrypted) secrets in (albeit private) repositories on the cloud, and I trust them more in encrypted local backups. This requirement automatically excludes both sops-nix and agenix which would have the secrets encrypted in the repository.

Also, there is a bit of satisfaction in building and actually using a piece of software, so some motivation for this tool comes from here too I would guess.

Couldn't this be a bash script?

Yes, and that's the point of the "independence" / manual recovery: it shouldn't be doing anything that can't be replicated by hand in a terminal, or in a bash script.

So why isn't it a bash script? Originally, it was. I decided to move away from bash mainly for ergonomics and because I am not skilled enough in bash to pull off something like this. Also I want types. When I program I need types and I need them to be actual constraints that prevent me from shooting myself in the foot.

It probably didn't need to be in Rust, and something like Go (which still compiles to a standalone executable binary that can be added to the export bundle) would have been fine, but I don't know Go.

Conclusions

I developed this tool mainly to fit my needs, and I don't expect it to see widespread use. However, I do think that it can be useful for some. For me, it solves the problem of wanting automatic secrets management and deployment without having to commit the (encrypted) secrets to the dotfiles repository.