1{self}: {
2 config,
3 pkgs,
4 lib,
5 ...
6}: let
7 cfg = config.services.tangled-knot;
8in
9 with lib; {
10 options = {
11 services.tangled-knot = {
12 enable = mkOption {
13 type = types.bool;
14 default = false;
15 description = "Enable a tangled knot";
16 };
17
18 appviewEndpoint = mkOption {
19 type = types.str;
20 default = "https://tangled.sh";
21 description = "Appview endpoint";
22 };
23
24 gitUser = mkOption {
25 type = types.str;
26 default = "git";
27 description = "User that hosts git repos and performs git operations";
28 };
29
30 openFirewall = mkOption {
31 type = types.bool;
32 default = true;
33 description = "Open port 22 in the firewall for ssh";
34 };
35
36 stateDir = mkOption {
37 type = types.path;
38 default = "/home/${cfg.gitUser}";
39 description = "Tangled knot data directory";
40 };
41
42 repo = {
43 scanPath = mkOption {
44 type = types.path;
45 default = cfg.stateDir;
46 description = "Path where repositories are scanned from";
47 };
48
49 mainBranch = mkOption {
50 type = types.str;
51 default = "main";
52 description = "Default branch name for repositories";
53 };
54 };
55
56 server = {
57 listenAddr = mkOption {
58 type = types.str;
59 default = "0.0.0.0:5555";
60 description = "Address to listen on";
61 };
62
63 internalListenAddr = mkOption {
64 type = types.str;
65 default = "127.0.0.1:5444";
66 description = "Internal address for inter-service communication";
67 };
68
69 secretFile = mkOption {
70 type = lib.types.path;
71 example = "KNOT_SERVER_SECRET=<hash>";
72 description = "File containing secret key provided by appview (required)";
73 };
74
75 dbPath = mkOption {
76 type = types.path;
77 default = "${cfg.stateDir}/knotserver.db";
78 description = "Path to the database file";
79 };
80
81 hostname = mkOption {
82 type = types.str;
83 example = "knot.tangled.sh";
84 description = "Hostname for the server (required)";
85 };
86
87 dev = mkOption {
88 type = types.bool;
89 default = false;
90 description = "Enable development mode (disables signature verification)";
91 };
92 };
93 };
94 };
95
96 config = mkIf cfg.enable {
97 environment.systemPackages = with pkgs; [
98 git
99 self.packages."${pkgs.system}".knot
100 ];
101
102 system.activationScripts.gitConfig = ''
103 mkdir -p "${cfg.repo.scanPath}"
104 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.repo.scanPath}"
105
106 mkdir -p "${cfg.stateDir}/.config/git"
107 cat > "${cfg.stateDir}/.config/git/config" << EOF
108 [user]
109 name = Git User
110 email = git@example.com
111 EOF
112 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.stateDir}"
113 '';
114
115 users.users.${cfg.gitUser} = {
116 isSystemUser = true;
117 useDefaultShell = true;
118 home = cfg.stateDir;
119 createHome = true;
120 group = cfg.gitUser;
121 };
122
123 users.groups.${cfg.gitUser} = {};
124
125 services.openssh = {
126 enable = true;
127 extraConfig = ''
128 Match User ${cfg.gitUser}
129 AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper
130 AuthorizedKeysCommandUser nobody
131 '';
132 };
133
134 environment.etc."ssh/keyfetch_wrapper" = {
135 mode = "0555";
136 text = ''
137 #!${pkgs.stdenv.shell}
138 ${self.packages.${pkgs.system}.knot}/bin/knot keys \
139 -output authorized-keys \
140 -internal-api "http://${cfg.server.internalListenAddr}" \
141 -git-dir "${cfg.repo.scanPath}" \
142 -log-path /tmp/knotguard.log
143 '';
144 };
145
146 systemd.services.knot = {
147 description = "knot service";
148 after = ["network.target" "sshd.service"];
149 wantedBy = ["multi-user.target"];
150 serviceConfig = {
151 User = cfg.gitUser;
152 WorkingDirectory = cfg.stateDir;
153 Environment = [
154 "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}"
155 "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}"
156 "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}"
157 "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}"
158 "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}"
159 "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}"
160 "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}"
161 ];
162 EnvironmentFile = cfg.server.secretFile;
163 ExecStart = "${self.packages.${pkgs.system}.knot}/bin/knot server";
164 Restart = "always";
165 };
166 };
167
168 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22];
169 };
170 }