Loading nixos/modules/services/security/spire/agent-tpm.nix 0 → 100644 +34 −0 Original line number Diff line number Diff line { 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 ]; }; } nixos/modules/services/security/spire/agent.nix +2 −0 Original line number Diff line number Diff line Loading @@ -125,6 +125,8 @@ in }; }; imports = [ ./agent-tpm.nix ]; config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; Loading nixos/modules/services/security/spire/server-tpm.nix 0 → 100644 +59 −0 Original line number Diff line number Diff line { 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. ''; }; }; }; }; }; } ); }; } nixos/modules/services/security/spire/server.nix +2 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,8 @@ in { meta.maintainers = [ lib.maintainers.arianvp ]; imports = [ ./server-tpm.nix ]; options.services.spire.server = { enable = lib.mkEnableOption "SPIRE Server"; Loading nixos/tests/spire.nix +140 −25 Original line number Diff line number Diff line { 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"; Loading @@ -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; Loading @@ -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; Loading @@ -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) # 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'") server.succeed(f"spire-server entry create -socketPath ${adminSocket} -selector 'systemd:id:backdoor.service' -parentID {parent_id} -spiffeID {spiffe_id}") 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) 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}") 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) 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 with subtest("Test certificate authentication from agent node"): agent.succeed("spire-agent api fetch x509 -socketPath ${workloadSocket} -write .") # TODO: Add something to communicate with 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=30) test_agent("join_token", agent, provision_join_token) test_agent("tpm", tpmAgent, provision_tpm) ''; } Loading
nixos/modules/services/security/spire/agent-tpm.nix 0 → 100644 +34 −0 Original line number Diff line number Diff line { 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 ]; }; }
nixos/modules/services/security/spire/agent.nix +2 −0 Original line number Diff line number Diff line Loading @@ -125,6 +125,8 @@ in }; }; imports = [ ./agent-tpm.nix ]; config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; Loading
nixos/modules/services/security/spire/server-tpm.nix 0 → 100644 +59 −0 Original line number Diff line number Diff line { 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. ''; }; }; }; }; }; } ); }; }
nixos/modules/services/security/spire/server.nix +2 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,8 @@ in { meta.maintainers = [ lib.maintainers.arianvp ]; imports = [ ./server-tpm.nix ]; options.services.spire.server = { enable = lib.mkEnableOption "SPIRE Server"; Loading
nixos/tests/spire.nix +140 −25 Original line number Diff line number Diff line { 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"; Loading @@ -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; Loading @@ -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; Loading @@ -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) # 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'") server.succeed(f"spire-server entry create -socketPath ${adminSocket} -selector 'systemd:id:backdoor.service' -parentID {parent_id} -spiffeID {spiffe_id}") 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) 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}") 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) 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 with subtest("Test certificate authentication from agent node"): agent.succeed("spire-agent api fetch x509 -socketPath ${workloadSocket} -write .") # TODO: Add something to communicate with 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=30) test_agent("join_token", agent, provision_join_token) test_agent("tpm", tpmAgent, provision_tpm) ''; }