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