1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.step-ca;
9 settingsFormat = (pkgs.formats.json { });
10in
11{
12 meta.maintainers = [ ];
13
14 options = {
15 services.step-ca = {
16 enable = lib.mkEnableOption "the smallstep certificate authority server";
17 openFirewall = lib.mkEnableOption "opening the certificate authority server port";
18 package = lib.mkPackageOption pkgs "step-ca" { };
19 address = lib.mkOption {
20 type = lib.types.str;
21 example = "127.0.0.1";
22 description = ''
23 The address (without port) the certificate authority should listen at.
24 This combined with {option}`services.step-ca.port` overrides {option}`services.step-ca.settings.address`.
25 '';
26 };
27 port = lib.mkOption {
28 type = lib.types.port;
29 example = 8443;
30 description = ''
31 The port the certificate authority should listen on.
32 This combined with {option}`services.step-ca.address` overrides {option}`services.step-ca.settings.address`.
33 '';
34 };
35 settings = lib.mkOption {
36 type = with lib.types; attrsOf anything;
37 description = ''
38 Settings that go into {file}`ca.json`. See
39 [the step-ca manual](https://smallstep.com/docs/step-ca/configuration)
40 for more information. The easiest way to
41 configure this module would be to run `step ca init`
42 to generate {file}`ca.json` and then import it using
43 `builtins.fromJSON`.
44 [This article](https://smallstep.com/docs/step-cli/basic-crypto-operations#run-an-offline-x509-certificate-authority)
45 may also be useful if you want to customize certain aspects of
46 certificate generation for your CA.
47 You need to change the database storage path to {file}`/var/lib/step-ca/db`.
48
49 ::: {.warning}
50 The {option}`services.step-ca.settings.address` option
51 will be ignored and overwritten by
52 {option}`services.step-ca.address` and
53 {option}`services.step-ca.port`.
54 :::
55 '';
56 };
57 intermediatePasswordFile = lib.mkOption {
58 type = lib.types.pathWith {
59 inStore = false;
60 absolute = true;
61 };
62 example = "/run/keys/smallstep-password";
63 description = ''
64 Path to the file containing the password for the intermediate
65 certificate private key.
66
67 ::: {.warning}
68 Make sure to use a quoted absolute path instead of a path literal
69 to prevent it from being copied to the globally readable Nix
70 store.
71 :::
72 '';
73 };
74 };
75 };
76
77 config = lib.mkIf config.services.step-ca.enable (
78 let
79 configFile = settingsFormat.generate "ca.json" (
80 cfg.settings
81 // {
82 address = cfg.address + ":" + toString cfg.port;
83 }
84 );
85 in
86 {
87 systemd.packages = [ cfg.package ];
88
89 # configuration file indirection is needed to support reloading
90 environment.etc."smallstep/ca.json".source = configFile;
91
92 systemd.services."step-ca" = {
93 wantedBy = [ "multi-user.target" ];
94 restartTriggers = [ configFile ];
95 unitConfig = {
96 ConditionFileNotEmpty = ""; # override upstream
97 };
98 serviceConfig = {
99 User = "step-ca";
100 Group = "step-ca";
101 UMask = "0077";
102 Environment = "HOME=%S/step-ca";
103 WorkingDirectory = ""; # override upstream
104 ReadWritePaths = ""; # override upstream
105
106 # LocalCredential handles file permission problems arising from the use of DynamicUser.
107 LoadCredential = "intermediate_password:${cfg.intermediatePasswordFile}";
108
109 ExecStart = [
110 "" # override upstream
111 "${cfg.package}/bin/step-ca /etc/smallstep/ca.json --password-file \${CREDENTIALS_DIRECTORY}/intermediate_password"
112 ];
113
114 # ProtectProc = "invisible"; # not supported by upstream yet
115 # ProcSubset = "pid"; # not supported by upstream yet
116 # PrivateUsers = true; # doesn't work with privileged ports therefore not supported by upstream
117
118 DynamicUser = true;
119 StateDirectory = "step-ca";
120 };
121 };
122
123 users.users.step-ca = {
124 home = "/var/lib/step-ca";
125 group = "step-ca";
126 isSystemUser = true;
127 };
128
129 users.groups.step-ca = { };
130
131 networking.firewall = lib.mkIf cfg.openFirewall {
132 allowedTCPPorts = [ cfg.port ];
133 };
134 }
135 );
136}