# Edit this configuration file to define what should be installed on # your system. Help is available in the configuration.nix(5) man page # and in the NixOS manual (accessible by running `nixos-help`). { config, lib, pkgs, ... }: with lib; let netif = "eth0"; hostname = "linde"; net-ip4 = "116.203.248.56"; net-mask4 = 32; net-gw = "172.31.1.1"; prefix6 = "2a01:4f8:c012:23a4"; net-ip6 = "${prefix6}::1"; net-mask6 = 64; net-gw6 = "fe80::1"; domain = "alin.ovh"; oldDomain = "alanpearce.eu"; email = "alan@alanpearce.eu"; ts-domain = "fin-marlin.ts.net"; in { imports = [ # Include the results of the hardware scan. ./linde-hardware.nix ../roles/server.nix ../roles/reverse-proxy.nix ../roles/binary-cache.nix ../roles/forgejo-runner.nix ../roles/searchix.nix ../settings/services/git-server.nix ]; age.secrets = { paperless = let cfg = config.services.paperless; in { file = ../../secrets/paperless.age; path = "${cfg.dataDir}/nixos-paperless-secret-key"; owner = cfg.user; mode = "400"; symlink = false; }; powerdns.file = ../../secrets/powerdns.age; redis-website.file = ../../secrets/redis-website.age; cifs-jellyfin.file = ../../secrets/cifs-jellyfin.age; cifs-paperless.file = ../../secrets/cifs-paperless.age; ntfy.file = ../../secrets/ntfy.age; pocket-id.file = ../../secrets/pocket-id.age; readeck.file = ../../secrets/readeck.age; open-webui.file = ../../secrets/open-webui.age; }; boot.loader.efi.efiSysMountPoint = "/boot/efi"; environment.homeBinInPath = true; environment.localBinInPath = true; environment.systemPackages = with pkgs; [ cifs-utils lsof pdns litecli knot-dns nixpkgs-review nix-output-monitor ]; # Initial empty root password for easy login: users.users.root.initialHashedPassword = ""; programs.ssh.extraConfig = '' Host ${domain} git.${domain} Hostname git.${domain} User gitolite ''; nix = { settings = { max-jobs = 2; trusted-users = [ "root" "nixremote" ]; }; gc = { dates = "08:15"; }; optimise = { automatic = true; dates = [ "02:30" ]; }; buildMachines = [ { protocol = "ssh-ng"; sshUser = "nixremote"; hostName = "granatum.opia.ovh"; system = "aarch64-linux"; maxJobs = 2; speedFactor = 1; supportedFeatures = [ "kvm" ]; } ]; }; networking = { hostName = hostname; inherit domain; dhcpcd.enable = false; nameservers = [ "2606:4700:4700::1111" "2606:4700:4700::1001" "1.1.1.1" "1.0.0.1" ]; hosts = lib.mkForce { ${net-ip4} = [ "${hostname}.${domain}" hostname "redis" ]; ${net-ip6} = [ "${hostname}.${domain}" hostname "redis" ]; }; defaultGateway = { address = net-gw; interface = netif; }; defaultGateway6 = { address = net-gw6; interface = netif; }; interfaces.${netif} = { ipv4 = { addresses = [ { address = net-ip4; prefixLength = net-mask4; } ]; routes = [ { address = net-gw; prefixLength = 32; } ]; }; ipv6 = { addresses = [ { address = net-ip6; prefixLength = net-mask6; } ]; }; }; firewall = { allowedTCPPorts = [ 53 853 6379 9418 ]; allowedUDPPorts = [ 53 ]; trustedInterfaces = [ "podman0" ]; }; resolvconf = { enable = false; useLocalResolver = false; }; }; services.cloud-init.network.enable = false; services.resolved = { enable = true; settings.Resolve = { DNSSEC = true; }; }; systemd.network.wait-online.enable = lib.mkForce true; services.mycal = { enable = true; port = 8002; settings = { timezone = config.time.timeZone; name = "Alin"; inherit email; }; }; zramSwap = { enable = true; algorithm = "zstd"; }; boot.kernel.sysctl = let buffer_size = 16 * 1024 * 1024; server_count = 2; max_clients = 100; page_size = 4096; # This server might have 100 clients simultaneously, so: # max(tcp_wmem) * 2 * 100 / 4096 mem = toString (buffer_size * server_count * max_clients / page_size); in { "net.ipv4.tcp_allowed_congestion_control" = "bbr illinois reno"; "net.ipv4.tcp_congestion_control" = "bbr"; "net.core.default_qdisc" = "fq"; # Provide adequate buffer memory. # rmem_max and wmem_max are TCP max buffer size # settable with setsockopt(), in bytes # tcp_rmem and tcp_wmem are per socket in bytes. # tcp_mem is for all TCP streams, in 4096-byte pages. # The following are suggested on IBM's # High Performance Computing page "net.core.rmem_max" = buffer_size; "net.core.wmem_max" = buffer_size; "net.core.rmem_default" = buffer_size; "net.core.wmem_default" = buffer_size; "net.ipv4.tcp_rmem" = "4096 87380 ${toString buffer_size}"; "net.ipv4.tcp_wmem" = "4096 87380 ${toString buffer_size}"; "net.ipv4.tcp_mem" = "${mem} ${mem} ${mem}"; "net.ipv4.tcp_sack" = false; "net.ipv4.tcp_dsack" = false; "net.ipv4.tcp_slow_start_after_idle" = false; }; security.sudo.extraConfig = '' Defaults:root,%wheel env_keep+=EDITOR ''; users.users.root = { openssh.authorizedKeys.keys = [ "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHYUyDdw92TNXguAxcmcmZmn/7ECGdRp6ckjxU+5zCw3BCnsS5+xEvHBVnnFdJRoH2XpfMeJjE+fi67zFVhlbn4= root@secretive.marvin" ]; }; users.users.alin = { extraGroups = [ "wheel" "caddy" "docker" "podman" "laminar" ]; isNormalUser = true; home = "/home/alin"; createHome = true; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFp1R7ls50vzyCx8pZAwGepQUuos0ogewkIioiW1uMup alin@prefect" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8VIII+598QOBxi/52O1Kb19RdUdX0aZmS1/dNoyqc5 alan@hetzner.strongbox" "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBENXbdPPOq0mA+9bPklpUkikvIXjCEKE5IRwNPNMr5/Ta1J/ND737fexCjEb7YPoAJtaf0gD/JThQQi/RxDTvy4= alin@alin.ovh" ]; }; users.users.nixremote = { shell = "/bin/sh"; isNormalUser = true; home = "/var/lib/nixremote/"; createHome = true; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKGzXGgFm/4Da2KNl1wb7TC2YOu/baP/2eFVBaJY0Wq root@marvin" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIy9jFioBvV0JA0lc+De2N+vDOABGHgCECW6vkD33CE4 sourcehut" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII7sWEwsm8JZiJ0LUnjSt0Kg1RXypG6p5AzP/R2n5ca actions@github.com" ]; }; # This value determines the NixOS release from which the default # settings for stateful data, like file locations and database versions # on your system were taken. It's perfectly fine and recommended to leave # this value at the release version of the first install of this system. # Before changing this value read the documentation for this option # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). system.stateVersion = "23.05"; # Did you read the comment? services.goatcounter = { enable = true; address = "localhost"; port = 8082; proxy = true; extraArgs = [ "-db" "sqlite3+db/goatcounter.sqlite3" "-websocket" "-automigrate" "-email-from" "goatcounter@${domain}" "-smtp" "smtp://localhost:25" "-ratelimit" "api-count:100/1" ]; }; services.powerdns = let inherit (lib.lists) flatten; inherit (lib.strings) concatStringsSep; he = { notify = "216.218.130.2"; axfr = [ "216.218.133.2" "2001:470:600::2" ]; }; iplist = ips: concatStringsSep "," (flatten ips); in { enable = true; secretFile = config.age.secrets.powerdns.path; extraConfig = '' launch=gsqlite3 dnsupdate=yes allow-dnsupdate-from=0.0.0.0/0,::/0 only-notify= also-notify=${iplist [ he.notify ]} allow-axfr-ips=${iplist [ he.axfr ]} expand-alias=yes outgoing-axfr-expand-alias=yes resolver=1.1.1.1 local-address=${net-ip4} ${net-ip6} log-dns-details=no log-dns-queries=no loglevel=5 primary=yes send-signed-notify=no default-api-rectify=yes default-soa-edit=inception-increment api=yes # replaced by secretFile/envsubst api-key=$API_KEY gsqlite3-database=/var/db/pdns/zones.db gsqlite3-pragma-foreign-keys=yes gsqlite3-dnssec=yes ''; }; services.postfix = { enable = true; enableSubmission = true; submissionOptions = { smtpd_client_restrictions = "permit_inet_interfaces,reject"; smtpd_tls_security_level = "none"; }; enableSubmissions = true; virtual = '' @${config.networking.hostName}.${domain} alin alin ${email} ''; settings = { main = { mydomain = domain; mydestination = [ ]; inet_interfaces = "loopback-only"; smtp_bind_address = "0.0.0.0"; smtp_bind_address6 = "::"; }; }; }; services.rspamd = { enable = true; postfix = { enable = true; }; locals = { "outgoing.conf".text = '' outbound { priority = high; authenticated = yes; apply { groups_disabled = ["rbl", "spf", "dmarc"]; actions { greylist = null; } } } ''; "dkim_signing.conf".text = '' enabled = true; selector = "202602"; domain = "alin.ovh"; path = "/var/dkim/$domain.$selector.key"; ''; }; }; systemd.services.backup-gitolite = { startAt = "daily"; path = with pkgs; [ openssh ]; serviceConfig = { Type = "oneshot"; ExecStart = "${lib.getExe pkgs.rdiff-backup} --api-version 201 backup ${config.services.gitolite.dataDir} ${hostname}@r5.${ts-domain}::gitolite"; ExecStartPost = "-${lib.getExe pkgs.rdiff-backup} --api-version 201 remove increments --older-than 3M ${hostname}@r5.${ts-domain}::gitolite"; }; }; systemd.services.backup-paperless = { startAt = "daily"; path = with pkgs; [ openssh ]; serviceConfig = { Type = "oneshot"; WorkingDirectory = config.services.paperless.dataDir; ExecStart = [ "systemd-run --machine=papers sudo -u paperless ./paperless-manage document_exporter --delete --use-filename-format --no-archive --no-thumbnail --no-progress-bar ./export " "${lib.getExe pkgs.rdiff-backup} --api-version 201 backup /srv/paperless/export ${hostname}@r5.${ts-domain}::paperless" ]; ExecStartPost = "-${lib.getExe pkgs.rdiff-backup} --api-version 201 remove increments --older-than 3M ${hostname}@r5.${ts-domain}::paperless"; }; }; security.acme = { certs."${oldDomain}" = { extraDomainNames = [ "*.${oldDomain}" ]; }; certs."stats.${domain}" = { extraDomainNames = [ "*.stats.${domain}" ]; }; certs."redis.${domain}" = { group = "redis-website"; reloadServices = [ "redis-website" ]; }; }; services.caddy = { virtualHosts = let inherit (import ../../lib/caddy.nix { inherit lib; }) security-headers; in { "pdns.${domain}" = { extraConfig = '' log { output discard } reverse_proxy 127.0.0.1:8081 ''; }; "id.alin.ovh" = { extraConfig = '' encode zstd gzip reverse_proxy 127.0.0.1:1411 ''; }; "iam.alin.ovh" = { extraConfig = '' redir https://id.alin.ovh{uri} permanent ''; }; "files.${domain}" = { extraConfig = '' encode zstd gzip ${security-headers { }} root * /srv/http/files file_server browse ''; }; "cal.${domain}" = { extraConfig = let srv = config.services.mycal; in '' encode zstd gzip cache { ttl 15m } reverse_proxy ${srv.host}:${toString srv.port} ''; }; "stats.${domain}" = let srv = config.services.goatcounter; in { useACMEHost = "stats.${domain}"; serverAliases = [ "*.stats.${domain}" ]; extraConfig = '' reverse_proxy ${srv.address}:${toString srv.port} ''; }; "go.${domain}" = { extraConfig = '' encode zstd gzip ${security-headers { }} root * /srv/http/go.alin.ovh file_server ''; }; "go.${oldDomain}" = { extraConfig = '' encode zstd gzip ${security-headers { }} root * /srv/http/go file_server ''; }; "*.${oldDomain}" = { useACMEHost = "${oldDomain}"; extraConfig = '' redir https://{labels.2}.${domain}{uri} ''; }; "jf.${domain}" = { extraConfig = '' reverse_proxy localhost:8096 ''; }; "ntfy.${domain}" = { extraConfig = '' reverse_proxy ${config.services.ntfy-sh.settings.listen-http} ''; }; "read.${ts-domain}" = { extraConfig = '' bind tailscale/read tailscale_auth reverse_proxy { to unix//run/web/readeck.sock header_up X-Webauth-User {http.auth.user.tailscale_login} header_up X-Webauth-Email {http.auth.user.tailscale_user} } ''; }; "papers.${ts-domain}" = { extraConfig = '' bind tailscale/papers tailscale_auth reverse_proxy [::1]:${toString config.services.paperless.port} { header_up Remote-User {http.auth.user.tailscale_login} } ''; }; "chat.${ts-domain}" = { extraConfig = '' bind tailscale/chat tailscale_auth reverse_proxy localhost:${toString config.services.open-webui.port} { header_up X-Webauth-User {http.auth.user.tailscale_login} header_up X-Webauth-Email {http.auth.user.tailscale_user} header_up X-Webauth-Name {http.auth.user.tailscale_name} } ''; }; }; }; services.open-webui = { enable = true; environment = { ANONYMIZED_TELEMETRY = "False"; DO_NOT_TRACK = "True"; ENABLE_VERSION_UPDATE_CHECK = "False"; ENABLE_API_KEYS = "True"; WEBUI_URL = "https://chat.${ts-domain}"; WEBUI_AUTH_TRUSTED_EMAIL_HEADER = "X-Webauth-Email"; WEBUI_AUTH_TRUSTED_NAME_HEADER = "X-Webauth-User"; WEBUI_AUTH = "False"; CORS_ALLOW_ORIGIN = "https://chat.${ts-domain}"; OPENAI_API_BASE_URL = "https://openrouter.ai/api/vi"; }; }; networking.nat = { enable = true; internalInterfaces = [ "ve-+" ]; externalInterface = netif; enableIPv6 = true; }; users.groups.paperless.members = [ "alin" "syncthing" ]; fileSystems."/srv/paperless" = { device = "//u439959-sub3.your-storagebox.de/u439959-sub3"; fsType = "smb3"; options = let # prevents hanging on network split automount_opts = [ "x-systemd.automount" "noauto" "x-systemd.mount-timeout=5s" ]; uid = config.ids.uids.paperless; in automount_opts ++ [ "credentials=${config.age.secrets.cifs-paperless.path}" "seal" "multichannel" "nobrl" # needed for sqlite "forceuid" "forcegid" "uid=${toString uid}" "gid=${toString uid}" ]; }; services.paperless = { enable = true; address = "::1"; mediaDir = "/srv/paperless/media"; settings = { PAPERLESS_DBENGINE = "sqlite"; PAPERLESS_TIME_ZONE = config.time.timeZone; PAPERLESS_URL = "https://papers.${ts-domain}"; PAPERLESS_TRUSTED_PROXIES = "::1"; PAPERLESS_USE_X_FORWARD_HOST = true; PAPERLESS_USE_X_FORWARD_PORT = true; PAPERLESS_PROXY_SSL_HEADER = [ "HTTP_X_FORWARDED_PROTO" "https" ]; PAPERLESS_ENABLE_COMPRESSION = false; # let caddy do it PAPERLESS_ENABLE_HTTP_REMOTE_USER = true; PAPERLESS_ENABLE_HTTP_REMOTE_USER_API = true; PAPERLESS_OCR_SKIP_ARCHIVE_FILE = "with_text"; PAPERLESS_OCR_LANGUAGE = "deu+eng"; PAPERLESS_IGNORE_DATES = "09.08.90"; PAPERLESS_TASK_WORKERS = 2; PAPERLESS_THREADS_PER_WORKER = 1; PAPERLESS_NUMBER_OF_SUGGESTED_DATES = 4; PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ".stfolder/*" ".stversions/*" ]; PAPERLESS_FILENAME_FORMAT = "{correspondent}/{created} {title} {asn}"; PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = true; }; }; services.pocket-id = { enable = true; environmentFile = config.age.secrets.pocket-id.path; settings = { APP_URL = "https://id.alin.ovh"; TRUST_PROXY = true; }; }; systemd.services.readeck.serviceConfig = { SupplementaryGroups = [ "webservice" ]; }; services.readeck = { enable = true; package = pkgs.readeck.overrideAttrs (attrs: rec { version = "0.22.0-pre1"; src = pkgs.fetchFromCodeberg { owner = "readeck"; repo = "readeck"; rev = "12e39b6fe20d3949b249b1410b83ea8ea5043046"; hash = "sha256-QOnxTUlC+TaU/MSDSIg8T3cc7dScojVGVyM7kltljbA="; }; vendorHash = "sha256-2MB7v5oG/LcEKtgbFNxPXSI8TljpbqYUrI7pvu7m+e8="; npmDeps = pkgs.fetchNpmDeps { src = "${src}/web"; hash = "sha256-znUKRaUdx6GXD2YL6hs0iveaAAHQ8H9n4NHZFi331+g="; }; }); environmentFile = config.age.secrets.readeck.path; settings = { server = { host = "unix:/run/web/readeck.sock?mode=0660"; base_url = "https://read.${ts-domain}"; }; email = { host = "localhost"; port = 587; from = "readeck@${domain}"; from_noreply = "noreply+readeck@${domain}"; }; auth.forwarded = { enabled = true; provisioning = false; header_user = "X-Webauth-User"; header_email = "X-Webauth-Email"; }; }; }; services.redis = { servers = { website = { enable = true; port = 0; bind = null; databases = 1; maxclients = 20; requirePassFile = config.age.secrets.redis-website.path; settings = { notify-keyspace-events = "KEA"; tls-port = 6379; tls-cert-file = "/var/lib/acme/redis.${domain}/cert.pem"; tls-key-file = "/var/lib/acme/redis.${domain}/key.pem"; tls-ca-cert-file = "/etc/ssl/certs/ca-certificates.crt"; tls-auth-clients = false; }; }; }; }; services.syncthing = { enable = true; dataDir = "/srv/syncthing"; configDir = "/var/lib/syncthing"; guiAddress = "[::]:8384"; openDefaultPorts = true; overrideDevices = false; overrideFolders = false; }; services.ntfy-sh = { enable = true; settings = { base-url = "https://ntfy.alin.ovh"; behind-proxy = true; upstream-base-url = "https://ntfy.sh"; auth-default-access = "deny"; enable-login = true; require-login = true; }; environmentFile = config.age.secrets.ntfy.path; }; fileSystems."/srv/video" = { device = "//u439959-sub5.your-storagebox.de/u439959-sub5"; fsType = "smb3"; options = [ "x-systemd.automount" "noauto" "x-systemd.mount-timeout=5s" "credentials=${config.age.secrets.cifs-jellyfin.path}" "seal" "multichannel" "forceuid" "forcegid" "uid=jellyfin" "gid=jellyfin" ]; }; services.jellyfin = { enable = true; }; }