nixos/spire: add spire-tpm-plugin support
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.spire.agent;
|
||||
in
|
||||
{
|
||||
options.services.spire.agent.settings.plugins.NodeAttestor.tpm = lib.mkOption {
|
||||
default = null;
|
||||
description = "TPM 2.0 node attestation plugin. When set, automatically enables security.tpm2 and grants the spire-agent user access to the TPM device.";
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
freeformType = (pkgs.formats.hcl1 { }).type;
|
||||
options.plugin_cmd = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = lib.getExe' pkgs.spire-tpm-plugin "tpm_attestor_agent";
|
||||
defaultText = lib.literalExpression ''lib.getExe' pkgs.spire-tpm-plugin "tpm_attestor_agent"'';
|
||||
description = "Path to the TPM attestor agent plugin binary.";
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
config = lib.mkIf (cfg.enable && cfg.settings.plugins.NodeAttestor.tpm != null) {
|
||||
security.tpm2.enable = true;
|
||||
|
||||
systemd.services.spire-agent.serviceConfig.SupplementaryGroups = [
|
||||
config.security.tpm2.tssGroup
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -125,6 +125,8 @@ in
|
||||
};
|
||||
|
||||
};
|
||||
imports = [ ./agent-tpm.nix ];
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
format = pkgs.formats.hcl1 { };
|
||||
in
|
||||
{
|
||||
options.services.spire.server.settings.plugins.NodeAttestor.tpm = lib.mkOption {
|
||||
default = null;
|
||||
description = ''
|
||||
TPM 2.0 node attestation plugin from
|
||||
[spire-tpm-plugin](https://github.com/spiffe/spire-tpm-plugin).
|
||||
'';
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
plugin_cmd = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = lib.getExe' pkgs.spire-tpm-plugin "tpm_attestor_server";
|
||||
defaultText = lib.literalExpression ''lib.getExe' pkgs.spire-tpm-plugin "tpm_attestor_server"'';
|
||||
description = "Path to the TPM attestor server plugin binary.";
|
||||
};
|
||||
plugin_data = lib.mkOption {
|
||||
default = { };
|
||||
description = ''
|
||||
Plugin data for the TPM NodeAttestor. Either `ca_path`,
|
||||
`hash_path`, or both must be configured.
|
||||
'';
|
||||
type = lib.types.submodule {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
ca_path = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The path to the CA directory. Contains the manufacturer CA
|
||||
cert that signed the TPM's EK certificate in PEM or DER
|
||||
format.
|
||||
'';
|
||||
};
|
||||
hash_path = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The path to the Hash directory. Contains empty files named
|
||||
after the EK public key hash.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,8 @@ in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.arianvp ];
|
||||
|
||||
imports = [ ./server-tpm.nix ];
|
||||
|
||||
options.services.spire.server = {
|
||||
enable = lib.mkEnableOption "SPIRE Server";
|
||||
|
||||
|
||||
+140
-25
@@ -1,5 +1,51 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
trustDomain = "example.com";
|
||||
|
||||
# TODO: tpm-ek test has similar stuff. Also the whole tpm provisioning should probably
|
||||
# just use the vtpm provisioning commands in the future?
|
||||
|
||||
# OpenSSL config to sign the swtpm EK with TPM-specific certificate extensions
|
||||
ekSignConf = pkgs.writeText "ek-sign.cnf" ''
|
||||
[ tpm_policy ]
|
||||
basicConstraints = critical, CA:FALSE
|
||||
keyUsage = critical, keyEncipherment
|
||||
certificatePolicies = 2.23.133.2.1
|
||||
extendedKeyUsage = 2.23.133.8.1
|
||||
subjectAltName = ASN1:SEQUENCE:dirname_tpm
|
||||
|
||||
[ dirname_tpm ]
|
||||
seq = EXPLICIT:4,SEQUENCE:dirname_tpm_seq
|
||||
|
||||
[ dirname_tpm_seq ]
|
||||
set = SET:dirname_tpm_set
|
||||
|
||||
[ dirname_tpm_set ]
|
||||
seq.1 = SEQUENCE:dirname_tpm_seq_manufacturer
|
||||
seq.2 = SEQUENCE:dirname_tpm_seq_model
|
||||
seq.3 = SEQUENCE:dirname_tpm_seq_version
|
||||
|
||||
[dirname_tpm_seq_manufacturer]
|
||||
oid = OID:2.23.133.2.1
|
||||
str = UTF8:"id:53544D20"
|
||||
|
||||
[dirname_tpm_seq_model]
|
||||
oid = OID:2.23.133.2.2
|
||||
str = UTF8:"ST33HTPHAHD4"
|
||||
|
||||
[dirname_tpm_seq_version]
|
||||
oid = OID:2.23.133.2.3
|
||||
str = UTF8:"id:00010101"
|
||||
'';
|
||||
|
||||
agent =
|
||||
{ config, ... }:
|
||||
{
|
||||
environment.variables.SPIFFE_ENDPOINT_SOCKET =
|
||||
config.services.spire.agent.settings.agent.socket_path;
|
||||
virtualisation.credentials."spire.trust_bundle".source = "./trust_bundle";
|
||||
systemd.services.spire-agent.serviceConfig.ImportCredential = [ "spire.trust_bundle" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "spire";
|
||||
@@ -9,6 +55,7 @@ in
|
||||
{ config, ... }:
|
||||
{
|
||||
networking.domain = trustDomain;
|
||||
environment.etc."spire/server/certs/tpm-ca.crt".source = ./tpm-ek/ca.crt;
|
||||
services.spire.server = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
@@ -21,21 +68,17 @@ in
|
||||
connection_string = "$STATE_DIRECTORY/datastore.sqlite3";
|
||||
};
|
||||
NodeAttestor.join_token.plugin_data = { };
|
||||
NodeAttestor.tpm.plugin_data.ca_path = "/etc/spire/server/certs";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
agent = {
|
||||
virtualisation.credentials = {
|
||||
"spire.join_token".source = "./join_token";
|
||||
"spire.trust_bundle".source = "./trust_bundle";
|
||||
};
|
||||
imports = [ agent ];
|
||||
|
||||
systemd.services.spire-agent.serviceConfig.ImportCredential = [
|
||||
"spire.join_token"
|
||||
"spire.trust_bundle"
|
||||
];
|
||||
virtualisation.credentials."spire.join_token".source = "./join_token";
|
||||
systemd.services.spire-agent.serviceConfig.ImportCredential = [ "spire.join_token" ];
|
||||
|
||||
services.spire.agent = {
|
||||
enable = true;
|
||||
@@ -56,44 +99,116 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
tpmAgent =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [ agent ];
|
||||
virtualisation = {
|
||||
useEFIBoot = true;
|
||||
tpm = {
|
||||
enable = true;
|
||||
# Provision the swtpm with an EK certificate signed by testCA so that
|
||||
# the SPIRE server can verify the agent's identity.
|
||||
provisioning = ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
pkgs.openssl
|
||||
pkgs.tpm2-tools
|
||||
]
|
||||
}:$PATH
|
||||
|
||||
tpm2_createek -G rsa -u ek.pub -c ek.ctx -f pem
|
||||
|
||||
openssl x509 \
|
||||
-extfile ${ekSignConf} \
|
||||
-new -days 365 \
|
||||
-subj "/CN=swtpm-ekcert" \
|
||||
-extensions tpm_policy \
|
||||
-CA ${./tpm-ek/ca.crt} -CAkey ${./tpm-ek/ca.priv} \
|
||||
-out ekcert.der -outform der \
|
||||
-force_pubkey ek.pub
|
||||
|
||||
tpm2_nvdefine 0x01c00002 \
|
||||
-C o \
|
||||
-a "ownerread|policyread|policywrite|ownerwrite|authread|authwrite" \
|
||||
-s "$(wc -c < ekcert.der)"
|
||||
|
||||
tpm2_nvwrite 0x01c00002 -C o -i ekcert.der
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.spire-tpm-plugin ];
|
||||
|
||||
services.spire.agent = {
|
||||
enable = true;
|
||||
settings = {
|
||||
agent = {
|
||||
trust_domain = trustDomain;
|
||||
server_address = "server.${trustDomain}";
|
||||
trust_bundle_format = "pem";
|
||||
trust_bundle_path = "$CREDENTIALS_DIRECTORY/spire.trust_bundle";
|
||||
};
|
||||
plugins = {
|
||||
KeyManager.memory.plugin_data = { };
|
||||
NodeAttestor.tpm.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):
|
||||
spiffe_id = "spiffe://${trustDomain}/service/backdoor"
|
||||
|
||||
# expose as system credentials
|
||||
def provision_trust_bundle(agent):
|
||||
# TODO: instead of trust bundle to talk to the spire-server, use an upstream CA?
|
||||
bundle = server.succeed("spire-server bundle show -socketPath ${adminSocket}")
|
||||
with open(agent.state_dir / "trust_bundle", "w") as f:
|
||||
f.write(bundle)
|
||||
|
||||
|
||||
def provision_join_token(agent):
|
||||
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)
|
||||
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_id}")
|
||||
|
||||
|
||||
def provision_tpm(agent):
|
||||
agent.wait_for_unit("tpm2.target")
|
||||
ek_hash = agent.succeed("get_tpm_pubhash").strip()
|
||||
parent_id = f"spiffe://${trustDomain}/spire/agent/tpm/{ek_hash}"
|
||||
server.succeed(f"spire-server entry create -socketPath ${adminSocket} -selector 'systemd:id:backdoor.service' -parentID '{parent_id}' -spiffeID {spiffe_id}")
|
||||
|
||||
|
||||
def test_agent(name, agent_node, provision_fn):
|
||||
with subtest(f"Setup SPIRE agent with {name} attestation"):
|
||||
provision_trust_bundle(agent_node)
|
||||
provision_fn(agent_node)
|
||||
agent_node.wait_for_unit("spire-agent.service")
|
||||
agent_node.wait_until_succeeds("spire-agent healthcheck -socketPath $SPIFFE_ENDPOINT_SOCKET", timeout=90)
|
||||
with subtest(f"Test certificate authentication from {name} agent"):
|
||||
agent_node.wait_until_succeeds("spire-agent api fetch x509 -socketPath $SPIFFE_ENDPOINT_SOCKET -write .")
|
||||
# TODO: Add something to communicate with
|
||||
|
||||
# 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)
|
||||
server.wait_until_succeeds("spire-server healthcheck -socketPath ${adminSocket}", timeout=30)
|
||||
|
||||
|
||||
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)
|
||||
test_agent("join_token", agent, provision_join_token)
|
||||
test_agent("tpm", tpmAgent, provision_tpm)
|
||||
|
||||
|
||||
with subtest("Test certificate authentication from agent node"):
|
||||
agent.succeed("spire-agent api fetch x509 -socketPath ${workloadSocket} -write .")
|
||||
|
||||
# TODO: Add something to communicate with
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user