a more proper nixos module for the tangled knotserver
at knot-fix 257 lines 7.8 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 # 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}