Unverified Commit ab9160b3 authored by Arian van Putten's avatar Arian van Putten Committed by GitHub
Browse files

Add spire-tpm-plugin support to spire NixOS module (#514026)

parents 6c02740f e27ef537
Loading
Loading
Loading
Loading
+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
    ];
  };
}
+52 −9
Original line number Diff line number Diff line
@@ -26,28 +26,38 @@ in
          agent = {
            trust_domain = lib.mkOption {
              type = lib.types.str;
              description = "The trust domain that this agent belongs to";
              description = "The trust domain that this agent belongs to (should be no more than 255 characters)";
              example = "example.com";
            };
            data_dir = lib.mkOption {
              type = lib.types.str;
              default = "$STATE_DIRECTORY";
              description = "The directory where the SPIRE agent stores its data";
              description = "A directory the agent can use for its runtime data";
            };
            server_address = lib.mkOption {
              type = lib.types.str;
              description = "The address of the SPIRE server";
              description = "DNS name or IP 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";
              description = "Port number of the SPIRE server";
            };
            socket_path = lib.mkOption {
              type = lib.types.path;
              default = "/run/spire/agent/public/api.sock";
              description = "The path to the SPIRE agent socket";
              description = "Location to bind the SPIRE Agent API socket (Unix only)";
            };
            join_token = lib.mkOption {
              type = lib.types.nullOr lib.types.str;
              default = null;
              description = "An optional token which has been generated by the SPIRE server";
            };
            join_token_file = lib.mkOption {
              type = lib.types.nullOr lib.types.str;
              default = null;
              description = "Path to a file containing an optional join token which has been generated by the SPIRE server";
            };
          };
          plugins = lib.mkOption {
@@ -55,8 +65,39 @@ in
              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;
            type = lib.types.submodule {
              freeformType = format.type;
              options.NodeAttestor = lib.mkOption {
                default = { };
                description = ''
                  Gathers information used to attest the agent's identity to the server. Generally paired with a server plugin of the same type.
                '';
                type = lib.types.submodule {
                  freeformType = format.type;
                  options.join_token = lib.mkOption {
                    default = null;
                    description = ''
                      The `join_token` is responsible for attesting the agent's identity using a one-time-use pre-shared key.

                      Must be used in conjunction with the server-side `join_token` plugin.
                    '';
                    type = lib.types.nullOr (
                      lib.types.submodule {
                        freeformType = format.type;
                        options.plugin_data = lib.mkOption {
                          type = lib.types.submodule { };
                          default = { };
                          description = ''
                            As a special case for node attestors, the join token itself is configured by a CLI flag (`-joinToken`)
                            or by configuring `join_token` in the agent's main config body.
                          '';
                        };
                      }
                    );
                  };
                };
              };
            };
            example = {
              KeyManager.memory.plugin_data = { };
              NodeAttestor.join_token.plugin_data = { };
@@ -71,7 +112,7 @@ in
    configFile = lib.mkOption {
      type = lib.types.path;
      defaultText = "Config file generated from services.spire.agent.settings";
      default = format.generate "agent.conf" cfg.settings;
      default = format.generate "agent.conf" (lib.filterAttrsRecursive (_: v: v != null) cfg.settings);
      description = ''
        Path to the SPIRE agent configuration file. See [the documentation](https://spiffe.io/docs/latest/deploying/spire_agent/) for more information.
      '';
@@ -80,10 +121,12 @@ in
    expandEnv = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Expand environment variables in SPIRE config file";
      description = "Expand environment $VARIABLES in the config file";
    };

  };
  imports = [ ./agent-tpm.nix ];

  config = lib.mkIf cfg.enable {
    environment.systemPackages = [ cfg.package ];

+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.
                  '';
                };
              };
            };
          };
        };
      }
    );
  };
}
+32 −3
Original line number Diff line number Diff line
@@ -11,6 +11,8 @@ in
{
  meta.maintainers = [ lib.maintainers.arianvp ];

  imports = [ ./server-tpm.nix ];

  options.services.spire.server = {
    enable = lib.mkEnableOption "SPIRE Server";

@@ -59,8 +61,35 @@ in
              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 = lib.types.submodule {
              freeformType = format.type;
              options.NodeAttestor = lib.mkOption {
                default = { };
                description = ''
                  NodeAttestor plugins implement validation logic for nodes attempting to assert their identity.
                  They are generally paired with an agent plugin of the same type.
                  See [the documentation](https://spiffe.io/docs/latest/deploying/spire_server/#nodeattestor)
                  for the list of built-in NodeAttestor plugins.
                '';
                type = lib.types.submodule {
                  freeformType = format.type;
                  options.join_token = lib.mkOption {
                    default = null;
                    description = "Join token based node attestation.";
                    type = lib.types.nullOr (
                      lib.types.submodule {
                        freeformType = format.type;
                        options.plugin_data = lib.mkOption {
                          type = format.type;
                          default = { };
                          description = "Plugin data for the join_token NodeAttestor.";
                        };
                      }
                    );
                  };
                };
              };
            };
            example = {
              KeyManager.memory.plugin_data = { };
              DataStore.sql.plugin_data = {
@@ -76,7 +105,7 @@ in

    configFile = lib.mkOption {
      type = lib.types.path;
      default = format.generate "server.conf" cfg.settings;
      default = format.generate "server.conf" (lib.filterAttrsRecursive (_: v: v != null) 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.
+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";
@@ -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)

        # 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)

    '';
}