{ lib , pkgs , config , ... }: let cfg = config.server; hosted-domains = builtins.concatLists (builtins.attrValues (builtins.mapAttrs (k: v: [ k ] ++ v.serverAliases) config.services.nginx.virtualHosts)); in { imports = [ ./options.nix ./matrix.nix ./fdroid.nix ./mumble.nix ./mailserver.nix ./home.nix ./keycloak.nix ]; system.stateVersion = "22.11"; impermanence.directories = [ { directory = /var/www; } { directory = /secrets; mode = "0755"; } ]; networking.useDHCP = true; networking.firewall = { enable = true; allowedTCPPorts = lib.mkMerge [ [ # ssh 22 # http/s 80 443 ] (lib.mkIf config.services.unbound.enable [ # dns 53 853 ]) ]; allowedUDPPorts = lib.mkIf config.services.unbound.enable [ # dns 53 853 # quic 443 ]; }; # UNBOUND users.users.${config.common.mainUsername}.extraGroups = lib.mkIf config.services.unbound.enable [ config.services.unbound.group ]; networking.resolvconf.extraConfig = lib.mkIf config.services.unbound.enable '' name_servers="127.0.0.1 ::1" ''; services.unbound = { enable = false; package = pkgs.unbound-with-systemd.override { stdenv = pkgs.ccacheStdenv; withPythonModule = true; python = pkgs.python3; }; localControlSocketPath = "/run/unbound/unbound.ctl"; resolveLocalQueries = false; settings = { server = { interface = [ "0.0.0.0" "::" ]; access-control = [ "0.0.0.0/0 allow" "::/0 allow" ]; aggressive-nsec = true; do-ip6 = true; }; remote-control.control-enable = true; }; }; # just in case networking.hosts."127.0.0.1" = hosted-domains; networking.hosts."::1" = hosted-domains; services.postgresql.enable = true; services.postgresql.package = pkgs.postgresql_13; services.postgresql.extraPlugins = with pkgs.postgresql13Packages; [ tsja ]; # SSH services.openssh.enable = true; services.fail2ban = { enable = true; ignoreIP = lib.optionals (cfg.lanCidrV4 != "0.0.0.0/0") [ cfg.lanCidrV4 ] ++ (lib.optionals (cfg.lanCidrV6 != "::/0") [ cfg.lanCidrV6 ]); maxretry = 10; jails.dovecot = '' enabled = true filter = dovecot ''; }; # SEARXNG services.searx.enable = true; services.searx.package = pkgs.searxng; services.searx.runInUwsgi = true; services.searx.uwsgiConfig = let inherit (config.services.searx) settings; in { socket = "${lib.quoteListenAddr settings.server.bind_address}:${toString settings.server.port}"; }; services.searx.environmentFile = /var/lib/searx/searx.env; services.searx.settings = { use_default_settings = true; search = { safe_search = 0; autocomplete = "duckduckgo"; # dbpedia, duckduckgo, google, startpage, swisscows, qwant, wikipedia - leave blank to turn off default_lang = ""; # leave blank to detect from browser info or use codes from languages.py }; server = { port = 8888; bind_address = "::1"; secret_key = "@SEARX_SECRET_KEY@"; base_url = "https://search.${cfg.domainName}/"; image_proxy = true; default_http_headers = { X-Content-Type-Options = "nosniff"; X-XSS-Protection = "1; mode=block"; X-Download-Options = "noopen"; X-Robots-Tag = "noindex, nofollow"; Referrer-Policy = "no-referrer"; }; }; outgoing = { request_timeout = 5.0; # default timeout in seconds, can be override by engine max_request_timeout = 15.0; # the maximum timeout in seconds pool_connections = 100; # Maximum number of allowable connections, or null pool_maxsize = 10; # Number of allowable keep-alive connections, or null enable_http2 = true; # See https://www.python-httpx.org/http2/ }; }; services.nginx.virtualHosts."search.${cfg.domainName}" = let inherit (config.services.searx) settings; in { quic = true; enableACME = true; forceSSL = true; # locations."/".proxyPass = "http://${lib.quoteListenAddr settings.server.bind_address}:${toString settings.server.port}"; locations."/".extraConfig = '' uwsgi_pass "${lib.quoteListenAddr settings.server.bind_address}:${toString settings.server.port}"; include ${config.services.nginx.package}/conf/uwsgi_params; ''; }; # NGINX services.nginx.enable = true; services.nginx.enableReload = true; services.nginx.package = pkgs.nginxQuic; /* DNS over TLS services.nginx.streamConfig = let inherit (config.security.acme.certs."${cfg.domainName}") directory; in '' upstream dns { zone dns 64k; server 127.0.0.1:53; } server { listen 853 ssl; ssl_certificate ${directory}/fullchain.pem; ssl_certificate_key ${directory}/key.pem; ssl_trusted_certificate ${directory}/chain.pem; proxy_pass dns; } '';*/ services.nginx.commonHttpConfig = '' log_format postdata '{\"ip\":\"$remote_addr\",\"time\":\"$time_iso8601\",\"referer\":\"$http_referer\",\"body\":\"$request_body\",\"ua\":\"$http_user_agent\"}'; ${lib.concatMapStringsSep "\n" (x: "set_real_ip_from ${x};") (lib.splitString "\n" '' ${builtins.readFile (builtins.fetchurl { url = "https://www.cloudflare.com/ips-v4"; sha256 = "0ywy9sg7spafi3gm9q5wb59lbiq0swvf0q3iazl0maq1pj1nsb7h"; })} ${builtins.readFile (builtins.fetchurl { url = "https://www.cloudflare.com/ips-v6"; sha256 = "1ad09hijignj6zlqvdjxv7rjj8567z357zfavv201b9vx3ikk7cy"; })}'')} real_ip_header CF-Connecting-IP; ''; # brotli and zstd requires recompilation so I don't enable it # services.nginx.recommendedBrotliSettings = true; # services.nginx.recommendedZstdSettings = true; services.nginx.recommendedGzipSettings = true; services.nginx.recommendedOptimisation = true; services.nginx.recommendedProxySettings = true; services.nginx.recommendedTlsSettings = true; # BLOG services.nginx.virtualHosts.${cfg.domainName} = { quic = true; enableACME = true; serverAliases = [ "www.${cfg.domainName}" ]; forceSSL = true; extraConfig = "autoindex on;"; locations."/".root = "/var/www/${cfg.domainName}/"; locations."/src".root = "/var/www/${cfg.domainName}/"; locations."/src".extraConfig = "index force_dirlisting;"; locations."/submit_comment".extraConfig = '' access_log /var/log/nginx/comments.log postdata; proxy_pass https://${cfg.domainName}/submit.htm; break; ''; locations."/submit.htm" = { extraConfig = '' return 200 'Success!

Success! It may take a while for your comment to get moderated.

Please wait for 10 seconds until you get redirected back...

Or just go there manually.


'; ''; }; }; # GITEA services.nginx.virtualHosts."git.${cfg.domainName}" = let inherit (config.services.gitea) settings; in { quic = true; enableACME = true; forceSSL = true; locations."/".proxyPass = "http://${lib.quoteListenAddr settings.server.HTTP_ADDR}:${toString settings.server.HTTP_PORT}"; }; services.gitea = { enable = true; package = pkgs.forgejo; database = { createDatabase = false; passwordFile = "/var/lib/gitea/db_password"; type = "postgres"; }; settings = { federation.ENABLED = true; "git.timeout" = { DEFAULT = 6000; MIGRATE = 6000; MIRROR = 6000; GC = 120; }; mailer = { ENABLED = true; FROM = "Forgejo "; PROTOCOL = "smtp"; SMTP_ADDR = "mail.${cfg.domainName}"; SMTP_PORT = 587; USER = "noreply@${cfg.domainName}"; PASSWD = cfg.unhashedNoreplyPassword; FORCE_TRUST_SERVER_CERT = true; }; session = { COOKIE_SECURE = true; }; server = { ROOT_URL = "https://git.${cfg.domainName}"; HTTP_ADDR = "::1"; HTTP_PORT = 3310; DOMAIN = "git.${cfg.domainName}"; # START_SSH_SERVER = true; # SSH_PORT = 2222; }; service = { DISABLE_REGISTRATION = true; REGISTER_EMAIL_CONFIRM = true; }; }; }; # NEXTCLOUD services.nginx.virtualHosts."cloud.${cfg.domainName}" = { quic = true; enableACME = true; forceSSL = true; }; services.nextcloud = { enable = true; enableBrokenCiphersForSSE = false; package = pkgs.nextcloud27; autoUpdateApps.enable = true; # TODO: use socket auth and remove the next line database.createLocally = false; config = { adminpassFile = "/var/lib/nextcloud/admin_password"; dbpassFile = "/var/lib/nextcloud/db_password"; dbtype = "pgsql"; dbhost = "/run/postgresql"; overwriteProtocol = "https"; }; hostName = "cloud.${cfg.domainName}"; https = true; }; # AKKOMA # TODO: remove this in 2024 services.nginx.virtualHosts."pleroma.${cfg.domainName}" = { quic = true; enableACME = true; addSSL = true; serverAliases = [ "akkoma.${cfg.domainName}" ]; locations."/".return = "301 https://fedi.${cfg.domainName}$request_uri"; }; services.akkoma = { enable = true; dist.extraFlags = [ "+sbwt" "none" "+sbwtdcpu" "none" "+sbwtdio" "none" ]; config.":pleroma"."Pleroma.Web.Endpoint" = { url = { scheme = "https"; host = "fedi.${cfg.domainName}"; port = 443; }; secret_key_base._secret = "/secrets/akkoma/secret_key_base"; signing_salt._secret = "/secrets/akkoma/signing_salt"; live_view.signing_salt._secret = "/secrets/akkoma/live_view_signing_salt"; }; extraStatic."static/terms-of-service.html" = pkgs.writeText "terms-of-service.html" '' no bigotry kthx ''; initDb = { enable = false; username = "akkoma"; password._secret = "/secrets/akkoma/postgres_password"; }; config.":pleroma".":instance" = { name = cfg.domainName; description = "Insert instance description here"; email = "webmaster-akkoma@${cfg.domainName}"; notify_email = "noreply@${cfg.domainName}"; limit = 5000; registrations_open = true; }; config.":pleroma"."Pleroma.Repo" = { adapter = (pkgs.formats.elixirConf { }).lib.mkRaw "Ecto.Adapters.Postgres"; username = "akkoma"; password._secret = "/secrets/akkoma/postgres_password"; database = "akkoma"; hostname = "localhost"; }; config.":web_push_encryption".":vapid_details" = { subject = "mailto:webmaster-akkoma@${cfg.domainName}"; public_key._secret = "/secrets/akkoma/push_public_key"; private_key._secret = "/secrets/akkoma/push_private_key"; }; config.":joken".":default_signer"._secret = "/secrets/akkoma/joken_signer"; nginx = { quic = true; enableACME = true; forceSSL = true; }; }; systemd.services.akkoma = { path = [ pkgs.exiftool pkgs.gawk ]; serviceConfig.Restart = "on-failure"; unitConfig = { StartLimitIntervalSec = 60; StartLimitBurst = 3; }; }; # TODO: create a separate group for nginx and certspotter # TODO: calc tbs instead of pubkey hash? users.users.certspotter.extraGroups = [ "nginx" ]; services.certspotter = { enable = true; extraFlags = [ ]; watchlist = [ ".pavluk.org" ]; hooks = let openssl = "${pkgs.openssl.bin}/bin/openssl"; in lib.toList (pkgs.writeShellScript "certspotter-hook" '' if [[ "$EVENT" == discovered_cert ]]; then mkdir -p /var/lib/certspotter/allowed_keys for cert in $(find /var/lib/acme -regex ".*/fullchain.pem"); do hash="$(${openssl} x509 -in "$cert" -pubkey -noout | ${openssl} pkey -pubin -outform DER | ${openssl} sha256 | cut -d" " -f2)" touch "/var/lib/certspotter/allowed_keys/$hash" done [[ -f "/var/lib/certspotter/allowed_keys/$PUBKEY_SHA256" ]] && exit 0 fi (echo "Subject: $SUMMARY" && echo && cat "$TEXT_FILENAME") | /run/wrappers/bin/sendmail -i webmaster-certspotter@${cfg.domainName} ''); }; /*locations."/dns-query".extraConfig = '' grpc_pass grpc://127.0.0.1:53453; '';*/ # TODO: firefox sync? }