comment: true title: Manage Secrets description: Manage keys, secrets, files, and other out-of-store paths with wire Tool.#
{{ $frontmatter.title }}#
{{ $frontmatter.description }}
Introduction#
wire Tool is very unopinionated as to how you encrypt your secrets, wire only handles pushing and setting up permissions of your key files.
The source of your key can be a literal string (unencrypted), a path
(unencrypted), or a command that wire runs to evaluate the key. Programs that
work well with wire keys include:
- GPG
- Age
- Anything that non-interactively decrypts to
stdout.
Prerequisites#
wire uses a Rust binary to receive encrypted key data, so your deploying user must be trusted or you must add garnix as a trusted public key:
{ config, ... }:
{
nix.settings.trusted-users = [
config.deployment.target.user # [!code ++]
];
}
Otherwise, you may see errors such as:
error: cannot add path '/nix/store/...-wire-tool-agent-x86_64-linux-...' because it lacks a signature by a trusted key
This is a requirement because nix copy is used to copy the binary.
As a benefit to this approach, key deployments are significantly faster!
A Trivial "Key"#
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
node-1 = {
deployment.key."file.txt" = {
source = ''
Hello World!
'';
};
};
}
[user@node-1]$ cat /run/keys/file.txt
Hello World!
Encrypting with GPG#
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
node-1 = {
deployment.key."file.txt" = {
source = [
"gpg"
"--decrypt"
"${./secrets/file.txt.gpg}"
];
};
};
}
[user@node-1]$ cat /run/keys/file.txt
Hello World!
Encrypting with KeepassXC#
A simple example of extracting a KeepassXC attachment into a wire key.
You must pass the password through stdin as the command must be non-interactive.
Note that the --stdout is important as wire expects the command to output the key to stdout.
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
node-1 = {
deployment.key."file.txt" = {
source = [
"bash"
"-c"
''cat ~/pass | keepassxc-cli attachment-export --stdout ~/.local/share/keepass/database.kdbx test 'file.txt'''
];
};
};
}
[user@node-1]$ cat /run/keys/file.txt
Hello World!
A Plain Text File#
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
node-1 = {
deployment.key."file.txt" = {
# using this syntax will enter the file into the store, readable by
# anyone!
source = ./file.txt;
};
};
}
Persistence#
wire defaults destDir to /run/keys. /run/ is held in memory and will not
persist past reboot. Change
deployment.key.<name>.destDir
to something like /etc/keys if you need secrets every time the machine boots.
Upload Order#
By default wire will upload keys before the system is activated. You can
force wire to upload the key after the system is activated by setting
deployment.keys.<name>.uploadAt
to post-activation.
Permissions and Ownership#
wire secrets are owned by user & group root (0600). You can change these
with the user and group option.
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
node-1 = {
deployment.key."file.txt" = {
source = [
"gpg"
"--decrypt"
"${./secrets/file.txt.gpg}"
];
user = "my-user";
group = "my-group";
};
};
}
Further Examples#
Using Keys With Services#
You can access the full absolute path of any key with
config.deployment.keys.<name>.path (auto-generated and read-only).
Keys also have a config.deployment.keys.<name>.service property
(auto-generated and read-only), which represent systemd services that you can
require, telling systemd there is a hard-dependency on that key for the
service to run.
Here's an example with the Tailscale service:
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
node-1 = {config, ...}: {
services.tailscale = {
enable = true;
# use deployment key path directly
authKeyFile = config.deployment.keys."tailscale.key".path;
};
deployment.keys."tailscale.key" = {
keyCommand = ["gpg" "--decrypt" "${./secrets/tailscale.key.gpg}"];
};
# The service will not start unless the key exists.
systemd.services.tailscaled-autoconnect.requires = [
config.deployment.keys."tailscale.key".service
];
};
}
Scoping a Key to a service account#
Additionally you can scope the key to the user that the service runs under, to
further reduce duplication using the config argument. Here's an example of
providing a certificate that is only readable by the caddy service.
let
sources = import ./npins;
wire = import sources.wire;
in wire.makeHive {
meta.nixpkgs = import sources.nixpkgs { };
some-web-server = {config, ...}: {
deployment.keys."some.host.pem" = {
keyCommand = ["gpg" "--decrypt" "${./some.host.pem.gpg}"];
destDir = "/etc/keys";
# inherit the user and group that caddy runs under
# the key will only readable by the caddy service
inherit (config.services.caddy) user group;
};
# ^^ repeat for `some.host.key`
services.caddy = {
virtualHosts."https://some.host".extraConfig = ''
tls ${config.deployment.keys."some.host.pem".path} ${config.deployment.keys."some.host.key".path}
'';
};
};
}