# modified from https://tangled.sh/@tangled.sh/core/blob/master/flake.nix tangledFlake: { config, pkgs, lib, ... }: let inherit (lib) mkOption mkEnableOption types mkIf optional ; cfg = config.services.tangled-knotserver; tangledPkgs = tangledFlake.packages.${pkgs.system}; keyfetch-wrapped = pkgs.runCommandCC "tangled-packages-wrapped" { nativeBuildInputs = [ pkgs.makeBinaryWrapper ]; } '' mkdir -p $out/bin makeBinaryWrapper ${lib.getExe' tangledPkgs.knot "knot"} $out/bin/keyfetch \ --add-flags "keys" \ --append-flags "-output authorized-keys" \ --append-flags "-internal-api=http://${cfg.server.internalListenAddr}" \ --append-flags "-git-dir=${cfg.repo.scanPath}" \ --append-flags "-log-path=/var/log/knotserver/repoguard.log" ''; in { imports = [ (lib.mkRenamedOptionModule ["services" "tangled-knotserver" "gitUser"] ["services" "tangled-knotserver" "user"]) ]; options = { services.tangled-knotserver = { enable = mkEnableOption "a tangled knot server"; openFirewall = mkOption { type = types.bool; default = false; description = "Whether to automatically configure the firewall to open necessary ports."; }; appviewEndpoint = mkOption { type = types.str; default = "https://tangled.sh"; description = "Appview endpoint"; }; user = mkOption { type = types.str; default = "git"; description = "User that runs the server, hosts git repos and performs git operations"; }; git = { name = mkOption { type = types.str; default = "Tangled Knot daemon"; description = "Git username for git operations that requires one."; }; email = mkOption { type = types.str; default = "knot@example.invalid"; description = "Git email address for git operations that requires one."; }; }; # TODO: should a `stateDirectory` option be added? repo = { scanPath = mkOption { type = types.path; default = "/var/lib/tangled-knot"; description = "Path where repositories are stored"; }; mainBranch = mkOption { type = types.str; default = "main"; description = "Default branch name for repositories"; }; }; server = { listenAddr = mkOption { type = types.str; default = "0.0.0.0:5555"; description = "Address to listen on"; }; internalListenAddr = mkOption { type = types.str; default = "127.0.0.1:5444"; description = "Internal address for inter-service communication"; }; dbPath = mkOption { type = types.path; default = "knotserver.db"; description = "Path to the database file"; }; hostname = mkOption { type = types.str; example = "knot.tangled.sh"; description = "Hostname for the server (required)"; }; dev = mkOption { type = types.bool; default = false; description = "Enable development mode (disables signature verification)"; internal = true; }; }; extraConfig = mkOption { type = types.attrsOf types.str; default = { }; example = lib.literalExpression '' { KNOT_SERVER_OWNER = "did:web:handle.invalid"; } ''; description = '' Additional environment variables. Use `environmentFile` for secrets. `KNOT_SERVER_OWNER` must be set for the program to work correctly. ''; }; extraSshdConfig = mkOption { type = types.lines; default = ""; example = '' Banner none PasswordAuthentication no KbdInteractiveAuthentication no ''; description = "Additional sshd_config options to set for the git user."; }; environmentFile = mkOption { type = types.nullOr types.path; default = null; example = "/etc/tangled/knotserver.env"; description = '' Environment file to set additional configuration and secrets for the knotserver. ''; }; }; }; config = mkIf cfg.enable { warnings = optional cfg.server.dev '' tangled-knotserver: development mode is enabled. This is not recommended in production as signature checks are disabled. ''; assertions = [ { assertion = tangledPkgs ? knot; 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."; } ]; environment.systemPackages = with pkgs; [ git ]; users.users.${cfg.user} = { createHome = true; home = cfg.repo.scanPath; group = cfg.user; isSystemUser = true; useDefaultShell = true; }; users.groups.${cfg.user} = { }; systemd.services.knotserver = { description = "knotserver service"; path = [pkgs.git]; after = [ "network-online.target" "sshd.service" ]; wants = [ "network-online.target" "sshd.service" ]; wantedBy = [ "multi-user.target" ]; preStart = '' git config --global user.name "${cfg.git.name}" git config --global user.email "${cfg.git.email}" ''; serviceConfig = { User = cfg.user; WorkingDirectory = cfg.repo.scanPath; ExecStart = if (tangledPkgs ? knotserver) # compat then lib.getExe' tangledPkgs.knotserver "knotserver" else "${lib.getExe' tangledPkgs.knot "knot"} server"; Restart = "always"; StateDirectory = mkIf (lib.hasPrefix "/var/lib/tangled-knot" cfg.repo.scanPath) "tangled-knot"; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; # TODO: hardening }; environment = { APPVIEW_ENDPOINT = cfg.appviewEndpoint; KNOT_REPO_SCAN_PATH = cfg.repo.scanPath; KNOT_SERVER_INTERNAL_LISTEN_ADDR = cfg.server.internalListenAddr; KNOT_SERVER_LISTEN_ADDR = cfg.server.listenAddr; KNOT_SERVER_HOSTNAME = cfg.server.hostname; } // cfg.extraConfig; }; systemd.tmpfiles.settings."knotserver-settings"."/var/log/knotserver"."d" = { mode = "0750"; user = config.users.users.${cfg.user}.name; group = config.users.groups.${cfg.user}.name; }; services.openssh = { enable = true; # required for the module to actually function extraConfig = '' Match User ${cfg.user} AuthorizedKeysCommand ${config.security.wrapperDir}/keyfetch AuthorizedKeysCommandUser nobody ${cfg.extraSshdConfig} ''; }; # get around openssh restrictions security.wrappers.keyfetch = { owner = "root"; group = config.users.groups.${cfg.user}.name; permissions = "u+rx,go+x"; source = lib.getExe' keyfetch-wrapped "keyfetch"; }; # open firewall ports if configured networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; }; }