forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 8.5 kB view raw
1{ 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 package = mkOption { 19 type = types.package; 20 description = "Package to use for the knot"; 21 }; 22 23 appviewEndpoint = mkOption { 24 type = types.str; 25 default = "https://tangled.org"; 26 description = "Appview endpoint"; 27 }; 28 29 gitUser = mkOption { 30 type = types.str; 31 default = "git"; 32 description = "User that hosts git repos and performs git operations"; 33 }; 34 35 openFirewall = mkOption { 36 type = types.bool; 37 default = true; 38 description = "Open port 22 in the firewall for ssh"; 39 }; 40 41 stateDir = mkOption { 42 type = types.path; 43 default = "/home/${cfg.gitUser}"; 44 description = "Tangled knot data directory"; 45 }; 46 47 repo = { 48 scanPath = mkOption { 49 type = types.path; 50 default = cfg.stateDir; 51 description = "Path where repositories are scanned from"; 52 }; 53 54 readme = mkOption { 55 type = types.listOf types.str; 56 default = [ 57 "README.md" 58 "readme.md" 59 "README" 60 "readme" 61 "README.markdown" 62 "readme.markdown" 63 "README.txt" 64 "readme.txt" 65 "README.rst" 66 "readme.rst" 67 "README.org" 68 "readme.org" 69 "README.asciidoc" 70 "readme.asciidoc" 71 ]; 72 description = "List of README filenames to look for (in priority order)"; 73 }; 74 75 mainBranch = mkOption { 76 type = types.str; 77 default = "main"; 78 description = "Default branch name for repositories"; 79 }; 80 }; 81 82 git = { 83 userName = mkOption { 84 type = types.str; 85 default = "Tangled"; 86 description = "Git user name used as committer"; 87 }; 88 89 userEmail = mkOption { 90 type = types.str; 91 default = "noreply@tangled.org"; 92 description = "Git user email used as committer"; 93 }; 94 }; 95 96 motd = mkOption { 97 type = types.nullOr types.str; 98 default = null; 99 description = '' 100 Message of the day 101 102 The contents are shown as-is; eg. you will want to add a newline if 103 setting a non-empty message since the knot won't do this for you. 104 ''; 105 }; 106 107 motdFile = mkOption { 108 type = types.nullOr types.path; 109 default = null; 110 description = '' 111 File containing message of the day 112 113 The contents are shown as-is; eg. you will want to add a newline if 114 setting a non-empty message since the knot won't do this for you. 115 ''; 116 }; 117 118 server = { 119 listenAddr = mkOption { 120 type = types.str; 121 default = "0.0.0.0:5555"; 122 description = "Address to listen on"; 123 }; 124 125 internalListenAddr = mkOption { 126 type = types.str; 127 default = "127.0.0.1:5444"; 128 description = "Internal address for inter-service communication"; 129 }; 130 131 owner = mkOption { 132 type = types.str; 133 example = "did:plc:qfpnj4og54vl56wngdriaxug"; 134 description = "DID of owner (required)"; 135 }; 136 137 dbPath = mkOption { 138 type = types.path; 139 default = "${cfg.stateDir}/knotserver.db"; 140 description = "Path to the database file"; 141 }; 142 143 hostname = mkOption { 144 type = types.str; 145 example = "my.knot.com"; 146 description = "Hostname for the server (required)"; 147 }; 148 149 plcUrl = mkOption { 150 type = types.str; 151 default = "https://plc.directory"; 152 description = "atproto PLC directory"; 153 }; 154 155 jetstreamEndpoint = mkOption { 156 type = types.str; 157 default = "wss://jetstream1.us-west.bsky.network/subscribe"; 158 description = "Jetstream endpoint to subscribe to"; 159 }; 160 161 logDids = mkOption { 162 type = types.bool; 163 default = true; 164 description = "Enable logging of DIDs"; 165 }; 166 167 dev = mkOption { 168 type = types.bool; 169 default = false; 170 description = "Enable development mode (disables signature verification)"; 171 }; 172 }; 173 }; 174 }; 175 176 config = mkIf cfg.enable { 177 environment.systemPackages = [ 178 pkgs.git 179 cfg.package 180 ]; 181 182 users.users.${cfg.gitUser} = { 183 isSystemUser = true; 184 useDefaultShell = true; 185 home = cfg.stateDir; 186 createHome = true; 187 group = cfg.gitUser; 188 }; 189 190 users.groups.${cfg.gitUser} = {}; 191 192 services.openssh = { 193 enable = true; 194 extraConfig = '' 195 Match User ${cfg.gitUser} 196 AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 197 AuthorizedKeysCommandUser nobody 198 ChallengeResponseAuthentication no 199 PasswordAuthentication no 200 ''; 201 }; 202 203 environment.etc."ssh/keyfetch_wrapper" = { 204 mode = "0555"; 205 text = '' 206 #!${pkgs.stdenv.shell} 207 ${cfg.package}/bin/knot keys \ 208 -output authorized-keys \ 209 -internal-api "http://${cfg.server.internalListenAddr}" \ 210 -git-dir "${cfg.repo.scanPath}" \ 211 -log-path /tmp/knotguard.log 212 ''; 213 }; 214 215 systemd.services.knot = { 216 description = "knot service"; 217 after = ["network.target" "sshd.service"]; 218 wantedBy = ["multi-user.target"]; 219 enableStrictShellChecks = true; 220 221 preStart = let 222 setMotd = 223 if cfg.motdFile != null && cfg.motd != null 224 then throw "motdFile and motd cannot be both set" 225 else '' 226 ${optionalString (cfg.motdFile != null) "cat ${cfg.motdFile} > ${cfg.stateDir}/motd"} 227 ${optionalString (cfg.motd != null) ''printf "${cfg.motd}" > ${cfg.stateDir}/motd''} 228 ''; 229 in '' 230 mkdir -p "${cfg.repo.scanPath}" 231 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.repo.scanPath}" 232 233 mkdir -p "${cfg.stateDir}/.config/git" 234 cat > "${cfg.stateDir}/.config/git/config" << EOF 235 [user] 236 name = ${cfg.git.userName} 237 email = ${cfg.git.userEmail} 238 [receive] 239 advertisePushOptions = true 240 [uploadpack] 241 allowFilter = true 242 EOF 243 ${setMotd} 244 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.stateDir}" 245 ''; 246 247 serviceConfig = { 248 User = cfg.gitUser; 249 PermissionsStartOnly = true; 250 WorkingDirectory = cfg.stateDir; 251 Environment = [ 252 "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 253 "KNOT_REPO_README=${concatStringsSep "," cfg.repo.readme}" 254 "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 255 "KNOT_GIT_USER_NAME=${cfg.git.userName}" 256 "KNOT_GIT_USER_EMAIL=${cfg.git.userEmail}" 257 "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 258 "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 259 "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 260 "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 261 "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 262 "KNOT_SERVER_PLC_URL=${cfg.server.plcUrl}" 263 "KNOT_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}" 264 "KNOT_SERVER_OWNER=${cfg.server.owner}" 265 "KNOT_SERVER_LOG_DIDS=${ 266 if cfg.server.logDids 267 then "true" 268 else "false" 269 }" 270 "KNOT_SERVER_DEV=${ 271 if cfg.server.dev 272 then "true" 273 else "false" 274 }" 275 ]; 276 ExecStart = "${cfg.package}/bin/knot server"; 277 Restart = "always"; 278 }; 279 }; 280 281 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; 282 }; 283 }