a more proper nixos module for the tangled knotserver
at mistress 7.4 kB view raw
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 KNOT_SERVER_OWNER = "did:web:handle.invalid"; 129 } 130 ''; 131 description = '' 132 Additional environment variables. Use `environmentFile` for secrets. 133 134 `KNOT_SERVER_OWNER` must be set for the program to work correctly. 135 ''; 136 }; 137 138 extraSshdConfig = mkOption { 139 type = types.lines; 140 default = ""; 141 example = '' 142 Banner none 143 PasswordAuthentication no 144 KbdInteractiveAuthentication no 145 ''; 146 description = "Additional sshd_config options to set for the git user."; 147 }; 148 149 environmentFile = mkOption { 150 type = types.nullOr types.path; 151 default = null; 152 example = "/etc/tangled/knotserver.env"; 153 description = '' 154 Environment file to set additional configuration and secrets for the knotserver. 155 ''; 156 }; 157 }; 158 }; 159 160 config = mkIf cfg.enable { 161 warnings = optional cfg.server.dev '' 162 tangled-knotserver: development mode is enabled. This is not recommended in production as signature checks are disabled. 163 ''; 164 165 assertions = [ 166 { 167 assertion = tangledPkgs ? knot; 168 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."; 169 } 170 ]; 171 172 environment.systemPackages = with pkgs; [ git ]; 173 174 users.users.${cfg.user} = { 175 createHome = true; 176 home = cfg.repo.scanPath; 177 group = cfg.user; 178 isSystemUser = true; 179 useDefaultShell = true; 180 }; 181 182 users.groups.${cfg.user} = { }; 183 184 systemd.services.knotserver = { 185 description = "knotserver service"; 186 path = [pkgs.git]; 187 after = [ 188 "network-online.target" 189 "sshd.service" 190 ]; 191 wants = [ 192 "network-online.target" 193 "sshd.service" 194 ]; 195 wantedBy = [ "multi-user.target" ]; 196 197 preStart = '' 198 git config --global user.name "${cfg.git.name}" 199 git config --global user.email "${cfg.git.email}" 200 ''; 201 202 serviceConfig = { 203 User = cfg.user; 204 WorkingDirectory = cfg.repo.scanPath; 205 ExecStart = if (tangledPkgs ? knotserver) # compat 206 then lib.getExe' tangledPkgs.knotserver "knotserver" 207 else "${lib.getExe' tangledPkgs.knot "knot"} server"; 208 Restart = "always"; 209 210 StateDirectory = mkIf (lib.hasPrefix "/var/lib/tangled-knot" cfg.repo.scanPath) "tangled-knot"; 211 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; 212 # TODO: hardening 213 }; 214 215 environment = { 216 APPVIEW_ENDPOINT = cfg.appviewEndpoint; 217 KNOT_REPO_SCAN_PATH = cfg.repo.scanPath; 218 KNOT_SERVER_INTERNAL_LISTEN_ADDR = cfg.server.internalListenAddr; 219 KNOT_SERVER_LISTEN_ADDR = cfg.server.listenAddr; 220 KNOT_SERVER_HOSTNAME = cfg.server.hostname; 221 } // cfg.extraConfig; 222 }; 223 224 systemd.tmpfiles.settings."knotserver-settings"."/var/log/knotserver"."d" = { 225 mode = "0750"; 226 user = config.users.users.${cfg.user}.name; 227 group = config.users.groups.${cfg.user}.name; 228 }; 229 230 services.openssh = { 231 enable = true; # required for the module to actually function 232 extraConfig = '' 233 Match User ${cfg.user} 234 AuthorizedKeysCommand ${config.security.wrapperDir}/keyfetch 235 AuthorizedKeysCommandUser nobody 236 ${cfg.extraSshdConfig} 237 ''; 238 }; 239 240 # get around openssh restrictions 241 security.wrappers.keyfetch = { 242 owner = "root"; 243 group = config.users.groups.${cfg.user}.name; 244 permissions = "u+rx,go+x"; 245 source = lib.getExe' keyfetch-wrapped "keyfetch"; 246 }; 247 248 # open firewall ports if configured 249 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; 250 }; 251}