nixos/spire: init

This commit is contained in:
Arian van Putten 2026-01-18 16:45:16 +01:00
commit e5d8795180
5 changed files with 345 additions and 0 deletions

View file

@ -1515,6 +1515,8 @@
./services/security/reaction.nix
./services/security/shibboleth-sp.nix
./services/security/sks.nix
./services/security/spire/agent.nix
./services/security/spire/server.nix
./services/security/ssh-agent-switcher.nix
./services/security/sshguard.nix
./services/security/sslmate-agent.nix

View file

@ -0,0 +1,123 @@
{
lib,
pkgs,
config,
...
}:
let
format = pkgs.formats.hcl1 { };
cfg = config.services.spire.agent;
in
{
meta.maintainers = [ lib.maintainers.arianvp ];
options.services.spire.agent = {
enable = lib.mkEnableOption "SPIRE agent";
package = lib.mkPackageOption pkgs "spire" { };
settings = lib.mkOption {
description = ''
SPIRE Agent configuration file options. See [the documentation](https://spiffe.io/docs/latest/deploying/spire_agent/) for all available options.
'';
type = lib.types.submodule {
freeformType = format.type;
options = {
agent = {
trust_domain = lib.mkOption {
type = lib.types.str;
description = "The trust domain that this agent belongs to";
example = "example.com";
};
data_dir = lib.mkOption {
type = lib.types.str;
default = "$STATE_DIRECTORY";
description = "The directory where the SPIRE agent stores its data";
};
server_address = lib.mkOption {
type = lib.types.str;
description = "The address of the SPIRE server";
example = "server.example.com";
};
server_port = lib.mkOption {
type = lib.types.port;
default = 8081;
description = "The port on which the SPIRE server is listening";
};
socket_path = lib.mkOption {
type = lib.types.path;
default = "/run/spire/agent/public/api.sock";
description = "The path to the SPIRE agent socket";
};
};
plugins = lib.mkOption {
description = ''
Built-in plugin types can be found at [the plugin types documentation](https://spiffe.io/docs/latest/deploying/spire_agent/#plugin-types).
See [plugin configuration](https://spiffe.io/docs/latest/deploying/spire_agent/#plugin-configuration) for options and how to configure external plugins.
'';
# TODO: We can probably enforce some of these constraints with a submodule
type = format.type;
example = {
KeyManager.memory.plugin_data = { };
NodeAttestor.join_token.plugin_data = { };
WorkloadAttestor.systemd.plugin_data = { };
WorkloadAttestor.unix.plugin_data = { };
};
};
};
};
};
configFile = lib.mkOption {
type = lib.types.path;
defaultText = "Config file generated from services.spire.agent.settings";
default = format.generate "agent.conf" cfg.settings;
description = ''
Path to the SPIRE agent configuration file. See [the documentation](https://spiffe.io/docs/latest/deploying/spire_agent/) for more information.
'';
};
expandEnv = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Expand environment variables in SPIRE config file";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
# TODO: Switch to DynamicUser once https://github.com/NixOS/nixpkgs/issues/299476 lands
users.users.spire-agent = {
isSystemUser = true;
group = "spire-agent";
};
users.groups.spire-agent = { };
systemd.services.spire-agent = {
wantedBy = [ "multi-user.target" ];
description = "SPIRE agent";
serviceConfig = {
ExecStart =
"${lib.getExe' cfg.package "spire-agent"} run "
+ lib.cli.toCommandLineShellGNU { } {
inherit (cfg) expandEnv;
config = cfg.configFile;
};
Restart = "on-failure";
StateDirectory = "spire/agent";
StateDirectoryMode = "0700";
RuntimeDirectory = "spire/agent";
# TODO: Switch to DynamicUser once https://github.com/NixOS/nixpkgs/issues/299476 lands
# Without it, the systemd plugin can not talk to dbus
# DynamicUser = true;
User = "spire-agent";
Group = "spire-agent";
UMask = "0027";
# TODO: Hardening
};
};
};
}

View file

@ -0,0 +1,120 @@
{
lib,
pkgs,
config,
...
}:
let
format = pkgs.formats.hcl1 { };
cfg = config.services.spire.server;
in
{
meta.maintainers = [ lib.maintainers.arianvp ];
options.services.spire.server = {
enable = lib.mkEnableOption "SPIRE Server";
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Whether to open firewall";
default = false;
};
settings = lib.mkOption {
description = ''
SPIRE Server configuration file options. See [the documentation](https://spiffe.io/docs/latest/deploying/spire_server/) for all available options.
'';
type = lib.types.submodule {
freeformType = format.type;
options = {
server = {
trust_domain = lib.mkOption {
type = lib.types.str;
description = "The trust domain that this server belongs to";
example = "example.com";
};
data_dir = lib.mkOption {
type = lib.types.str;
description = "The directory where SPIRE server stores its data";
default = "$STATE_DIRECTORY";
};
socket_path = lib.mkOption {
type = lib.types.str;
default = "/run/spire/server/private/api.sock";
description = "Path to bind the SPIRE Server API Socket to";
};
bind_address = lib.mkOption {
type = lib.types.str;
default = "[::]";
description = "The address on which the SPIRE server is listening";
};
bind_port = lib.mkOption {
type = lib.types.port;
default = 8081;
description = "The port on which the SPIRE server is listening";
};
};
plugins = lib.mkOption {
description = ''
Built-in plugin types can be found at [the plugin types documentation](https://spiffe.io/docs/latest/deploying/spire_server/#plugin-types).
See [plugin configuration](https://spiffe.io/docs/latest/deploying/spire_server/#plugin-configuration) for options and how to configure external plugins.
'';
# TODO: We can probably enforce some of these constraints with a submodule
type = format.type;
example = {
KeyManager.memory.plugin_data = { };
DataStore.sql.plugin_data = {
database_type = "sqlite3";
connection_string = "$STATE_DIRECTORY/datastore.sqlite3";
};
NodeAttestor.join_token.plugin_data = { };
};
};
};
};
};
configFile = lib.mkOption {
type = lib.types.path;
default = format.generate "server.conf" cfg.settings;
defaultText = "Config file generated from services.spire.server.settings";
description = ''
Path to the SPIRE server configuration file. See [the documentation](https://spiffe.io/docs/latest/deploying/spire_server/) for more information.
'';
};
expandEnv = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Expand environment variables in services.spire.server.settings and services.spire.server.configFile";
};
package = lib.mkPackageOption pkgs "spire" { };
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.settings.server.bind_port ];
environment.systemPackages = [ cfg.package ];
systemd.services.spire-server = {
wantedBy = [ "multi-user.target" ];
description = "SPIRE Server";
documentation = [ "https://spiffe.io/docs/latest/deploying/spire_server/" ];
serviceConfig = {
ExecStart =
"${lib.getExe' cfg.package "spire-server"} run "
+ lib.cli.toCommandLineShellGNU { } {
inherit (cfg) expandEnv;
config = cfg.configFile;
};
Restart = "on-failure";
StateDirectory = "spire/server";
StateDirectoryMode = "0700";
RuntimeDirectory = "spire/server";
DynamicUser = true;
UMask = "0027";
# TODO: hardening
};
};
};
}

View file

@ -1490,6 +1490,7 @@ in
spacecookie = runTest ./spacecookie.nix;
spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark { };
spiped = runTest ./spiped.nix;
spire = runTest ./spire.nix;
sqlite3-to-mysql = runTest ./sqlite3-to-mysql.nix;
squid = runTest ./squid.nix;
ssh-agent-auth = runTest ./ssh-agent-auth.nix;

99
nixos/tests/spire.nix Normal file
View file

@ -0,0 +1,99 @@
let
trustDomain = "example.com";
in
{
name = "spire";
nodes = {
server =
{ config, ... }:
{
networking.domain = trustDomain;
services.spire.server = {
enable = true;
openFirewall = true;
settings = {
server.trust_domain = trustDomain;
plugins = {
KeyManager.memory.plugin_data = { };
DataStore.sql.plugin_data = {
database_type = "sqlite3";
connection_string = "$STATE_DIRECTORY/datastore.sqlite3";
};
NodeAttestor.join_token.plugin_data = { };
};
};
};
};
agent = {
virtualisation.credentials = {
"spire.join_token".source = "./join_token";
"spire.trust_bundle".source = "./trust_bundle";
};
systemd.services.spire-agent.serviceConfig.ImportCredential = [
"spire.join_token"
"spire.trust_bundle"
];
services.spire.agent = {
enable = true;
settings = {
agent = {
trust_domain = trustDomain;
server_address = "server.${trustDomain}";
join_token_file = "$CREDENTIALS_DIRECTORY/spire.join_token";
trust_bundle_format = "pem";
trust_bundle_path = "$CREDENTIALS_DIRECTORY/spire.trust_bundle";
};
plugins = {
KeyManager.memory.plugin_data = { };
NodeAttestor.join_token.plugin_data = { };
WorkloadAttestor.systemd.plugin_data = { };
WorkloadAttestor.unix.plugin_data = { };
};
};
};
};
};
testScript =
{ nodes }:
let
adminSocket = nodes.server.services.spire.server.settings.server.socket_path;
workloadSocket = nodes.agent.services.spire.agent.settings.agent.socket_path;
in
''
# TODO: instead of trust bundle to talk to the spire-server, use an upstream CA?
def provision(agent, spiffe_id):
# expose as system credentials
bundle = server.succeed("spire-server bundle show -socketPath ${adminSocket}")
with open(agent.state_dir / "trust_bundle", "w") as f:
f.write(bundle)
join_token = server.succeed("spire-server token generate -socketPath ${adminSocket}").split()[1]
with open(agent.state_dir / "join_token", "w") as f:
f.write(join_token)
# register a workload on the node
parent_id=f"spiffe://${trustDomain}/spire/agent/join_token/{join_token}"
server.succeed(f"spire-server entry create -socketPath ${adminSocket} -selector 'systemd:id:backdoor.service' -parentID {parent_id} -spiffeID 'spiffe://${trustDomain}/service/backdoor'")
with subtest("SPIRE server startup and health checks"):
server.wait_for_unit("spire-server.service")
server.wait_until_succeeds("spire-server healthcheck -socketPath ${adminSocket}", timeout=5)
with subtest("Setup SPIRE agent on agent node"):
provision(agent, "spiffe://${trustDomain}/server/agent")
agent.wait_for_unit("spire-agent.service")
agent.wait_until_succeeds("spire-agent healthcheck -socketPath ${workloadSocket}", timeout=5)
with subtest("Test certificate authentication from agent node"):
agent.succeed("spire-agent api fetch x509 -socketPath ${workloadSocket} -write .")
# TODO: Add something to communicate with
'';
}