a more proper nixos module for the tangled knotserver
1# modified from https://tangled.sh/@tangled.sh/core/blob/master/flake.nix
2tangledFlake:
3{
4 config,
5 pkgs,
6 lib,
7 ...
8}:
9let
10 inherit (lib)
11 mkOption
12 mkEnableOption
13 types
14 mkIf
15 optional
16 ;
17 cfg = config.services.tangled-knotserver;
18 tangledPkgs = tangledFlake.packages.${pkgs.system};
19
20 keyfetch-wrapped =
21 pkgs.runCommandCC "tangled-packages-wrapped" { nativeBuildInputs = [ pkgs.makeBinaryWrapper ]; }
22 ''
23 mkdir -p $out/bin
24
25 makeBinaryWrapper ${lib.getExe' tangledPkgs.knot "knot"} $out/bin/keyfetch \
26 --add-flags "keys" \
27 --append-flags "-output authorized-keys" \
28 --append-flags "-internal-api=http://${cfg.server.internalListenAddr}" \
29 --append-flags "-git-dir=${cfg.repo.scanPath}" \
30 --append-flags "-log-path=/var/log/knotserver/repoguard.log"
31 '';
32
33in
34{
35 imports = [
36 (lib.mkRenamedOptionModule ["services" "tangled-knotserver" "gitUser"] ["services" "tangled-knotserver" "user"])
37 ];
38
39 options = {
40 services.tangled-knotserver = {
41 enable = mkEnableOption "a tangled knot server";
42
43 openFirewall = mkOption {
44 type = types.bool;
45 default = false;
46 description = "Whether to automatically configure the firewall to open necessary ports.";
47 };
48
49 appviewEndpoint = mkOption {
50 type = types.str;
51 default = "https://tangled.sh";
52 description = "Appview endpoint";
53 };
54
55 user = mkOption {
56 type = types.str;
57 default = "git";
58 description = "User that runs the server, hosts git repos and performs git operations";
59 };
60
61 git = {
62 name = mkOption {
63 type = types.str;
64 default = "Tangled Knot daemon";
65 description = "Git username for git operations that requires one.";
66 };
67 email = mkOption {
68 type = types.str;
69 default = "knot@example.invalid";
70 description = "Git email address for git operations that requires one.";
71 };
72 };
73
74 # TODO: should a `stateDirectory` option be added?
75
76 repo = {
77 scanPath = mkOption {
78 type = types.path;
79 default = "/var/lib/tangled-knot";
80 description = "Path where repositories are stored";
81 };
82
83 mainBranch = mkOption {
84 type = types.str;
85 default = "main";
86 description = "Default branch name for repositories";
87 };
88 };
89
90 server = {
91 listenAddr = mkOption {
92 type = types.str;
93 default = "0.0.0.0:5555";
94 description = "Address to listen on";
95 };
96
97 internalListenAddr = mkOption {
98 type = types.str;
99 default = "127.0.0.1:5444";
100 description = "Internal address for inter-service communication";
101 };
102
103 dbPath = mkOption {
104 type = types.path;
105 default = "knotserver.db";
106 description = "Path to the database file";
107 };
108
109 hostname = mkOption {
110 type = types.str;
111 example = "knot.tangled.sh";
112 description = "Hostname for the server (required)";
113 };
114
115 dev = mkOption {
116 type = types.bool;
117 default = false;
118 description = "Enable development mode (disables signature verification)";
119 internal = true;
120 };
121 };
122
123 extraConfig = mkOption {
124 type = types.attrsOf types.str;
125 default = { };
126 example = lib.literalExpression ''
127 {
128 # this is only an example, do NOT do this! your secret will end up readable by *everyone*!
129 KNOT_SERVER_SECRET = "verysecuresecret";
130 }
131 '';
132 description = ''
133 Additional environment variables. Use `environmentFile` for secrets.
134
135 `KNOT_SERVER_SECRET` must be set for the knotserver to work, and can be obtained from
136 [this page](https://tangled.sh/knots). Please set this with environmentFile instead of setting it here
137 directly.
138 '';
139 };
140
141 extraSshdConfig = mkOption {
142 type = types.lines;
143 default = "";
144 example = ''
145 Banner none
146 PasswordAuthentication no
147 KbdInteractiveAuthentication no
148 '';
149 description = "Additional sshd_config options to set for the git user.";
150 };
151
152 environmentFile = mkOption {
153 type = types.nullOr types.path;
154 default = null;
155 example = "/etc/tangled/knotserver.env";
156 description = ''
157 Environment file to set additional configuration and secrets for the knotserver.
158
159 `KNOT_SERVER_SECRET` must be set for the knotserver to work, and can be obtained from
160 [this page](https://tangled.sh/knots).
161 '';
162 };
163 };
164 };
165
166 config = mkIf cfg.enable {
167 warnings = optional cfg.server.dev ''
168 tangled-knotserver: development mode is enabled. This is not recommended in production as signature checks are disabled.
169 '';
170
171 assertions = [
172 {
173 assertion = tangledPkgs ? knot;
174 message = "tangled-knotserver: your version of tangled flake is not compatible with this version of the knotserver module. please consider updating the pinned @tangled.sh/core version.";
175 }
176 ];
177
178 environment.systemPackages = with pkgs; [ git ];
179
180 users.users.${cfg.user} = {
181 createHome = true;
182 home = cfg.repo.scanPath;
183 group = cfg.user;
184 isSystemUser = true;
185 useDefaultShell = true;
186 };
187
188 users.groups.${cfg.user} = { };
189
190 systemd.services.knotserver = {
191 description = "knotserver service";
192 path = [pkgs.git];
193 after = [
194 "network-online.target"
195 "sshd.service"
196 ];
197 wants = [
198 "network-online.target"
199 "sshd.service"
200 ];
201 wantedBy = [ "multi-user.target" ];
202
203 preStart = ''
204 git config --global user.name "${cfg.git.name}"
205 git config --global user.email "${cfg.git.email}"
206 '';
207
208 serviceConfig = {
209 User = cfg.user;
210 WorkingDirectory = cfg.repo.scanPath;
211 ExecStart = if (tangledPkgs ? knotserver) # compat
212 then lib.getExe' tangledPkgs.knotserver "knotserver"
213 else "${lib.getExe' tangledPkgs.knot "knot"} server";
214 Restart = "always";
215
216 StateDirectory = mkIf (lib.hasPrefix "/var/lib/tangled-knot" cfg.repo.scanPath) "tangled-knot";
217 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
218 # TODO: hardening
219 };
220
221 environment = {
222 APPVIEW_ENDPOINT = cfg.appviewEndpoint;
223 KNOT_REPO_SCAN_PATH = cfg.repo.scanPath;
224 KNOT_SERVER_INTERNAL_LISTEN_ADDR = cfg.server.internalListenAddr;
225 KNOT_SERVER_LISTEN_ADDR = cfg.server.listenAddr;
226 KNOT_SERVER_HOSTNAME = cfg.server.hostname;
227 } // cfg.extraConfig;
228 };
229
230 systemd.tmpfiles.settings."knotserver-settings"."/var/log/knotserver"."d" = {
231 mode = "0750";
232 user = config.users.users.${cfg.user}.name;
233 group = config.users.groups.${cfg.user}.name;
234 };
235
236 services.openssh = {
237 enable = true; # required for the module to actually function
238 extraConfig = ''
239 Match User ${cfg.user}
240 AuthorizedKeysCommand ${config.security.wrapperDir}/keyfetch
241 AuthorizedKeysCommandUser nobody
242 ${cfg.extraSshdConfig}
243 '';
244 };
245
246 # get around openssh restrictions
247 security.wrappers.keyfetch = {
248 owner = "root";
249 group = config.users.groups.${cfg.user}.name;
250 permissions = "u+rx,go+x";
251 source = lib.getExe' keyfetch-wrapped "keyfetch";
252 };
253
254 # open firewall ports if configured
255 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22];
256 };
257}