{ config , pkgs , lib , ... }: # Gitea git server, publicly exposed through an external reverse proxy. # # Topology: # internet -> gitea.edley.me -> external reverse proxy (TLS terminator) # -> Tailscale -> homeserver-1:port (Gitea, plain HTTP behind the proxy) # # Hardening posture (public instance): # - HTTPS only (DISABLE_SSH) so there is no second public port to forward. # - Invite-only: registration disabled, everything hidden behind sign-in. # - The firewall admits ONLY the proxy's Tailscale IP; no other tailnet # node (or the LAN) can reach Gitea directly. # - X-Forwarded-* headers are trusted only from that proxy IP. # - PUBLIC_URL_DETECTION = never pins all generated URLs to ROOT_URL # (no Host-header hijacking). # - OpenID disabled, migrations disabled (SSRF reduction), argon2 password # hashing, 12-char minimum password, git hooks disabled. { options.services.gitea-server = { enable = lib.mkEnableOption "Gitea git server"; port = lib.mkOption { type = lib.types.port; default = 3000; description = "Port Gitea listens on. Firewalled to the reverse proxy only."; }; domain = lib.mkOption { type = lib.types.str; default = "gitea.edley.me"; description = "Public domain name of the instance."; }; proxyIp = lib.mkOption { type = lib.types.str; description = "Tailscale IP of the external reverse proxy allowed to reach Gitea."; }; }; config = lib.mkIf config.services.gitea-server.enable { services.gitea = { enable = true; stateDir = "/data/gitea"; appName = "gitea: Git with a cup of tea"; database = { type = "postgres"; createDatabase = true; name = "gitea"; user = "gitea"; }; dump = { enable = true; interval = "daily"; type = "tar.zst"; }; settings = { server = { DOMAIN = config.services.gitea-server.domain; ROOT_URL = "https://${config.services.gitea-server.domain}/"; HTTP_ADDR = "0.0.0.0"; HTTP_PORT = config.services.gitea-server.port; DISABLE_SSH = true; PUBLIC_URL_DETECTION = "never"; MINIMUM_KEY_SIZE_CHECK = true; }; session = { COOKIE_SECURE = true; }; service = { DISABLE_REGISTRATION = true; REQUIRE_SIGNIN_VIEW = true; SHOW_REGISTRATION_BUTTON = false; }; security = { REVERSE_PROXY_TRUSTED_PROXIES = config.services.gitea-server.proxyIp; REVERSE_PROXY_LIMIT = 1; MIN_PASSWORD_LENGTH = 12; PASSWORD_HASH_ALGO = "argon2"; DISABLE_GIT_HOOKS = true; }; openid = { ENABLE_OPENID_SIGNIN = false; ENABLE_OPENID_SIGNUP = false; }; repository = { DISABLE_MIGRATIONS = true; }; other = { SHOW_FOOTER_VERSION = false; SHOW_FOOTER_TEMPLATE_LOAD_TIME = false; }; }; }; # The external proxy (TLS terminator for gitea.edley.me) connects over # Tailscale. Admit ONLY its address on the Gitea port; the default input # policy drops everything else. Update proxyIp when the proxy changes. networking.firewall.extraInputRules = '' ip saddr ${config.services.gitea-server.proxyIp} tcp dport ${toString config.services.gitea-server.port} accept ''; }; }