Commit e5d87951 authored by Arian van Putten's avatar Arian van Putten
Browse files

nixos/spire: init

parent 7553fce9
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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
+123 −0
Original line number Diff line number Diff line
{
  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
      };
    };
  };
}
+120 −0
Original line number Diff line number Diff line
{
  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
      };
    };
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -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;

nixos/tests/spire.nix

0 → 100644
+99 −0
Original line number Diff line number Diff line
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
    '';
}