Harden server and add Nix-native CI + self-hosted Gitea Actions
- caddy: security headers (X-Content-Type-Options/X-XSS-Protection/ X-Frame-Options) on all vhosts + baseline CSP; strip SnappyMail upstream copies via header_down on mail.severijnse.eu - tlsa-updater: compute TLSA 3 1 1 from cert SPKI (SHA-256), sync _25/_465/_993, fail-safe placeholders; coredns zone updated - pre-commit: wire cachix/git-hooks.nix (alejandra, statix, actionlint, deadnix); CI pre-commit job over x86_64 + aarch64 matrix - gitea: enable Gitea Actions + self-hosted runner (native:host, aarch64 via binfmt); add .gitea/workflows/ci.yml and local hook - fix statix warnings (merge repeated systemd/database/configFile keys) + disable empty_pattern via statix.toml (nixpkgs standard) - Clean up unused lambda patterns across 38 .nix files via deadnix - Format whole repo with alejandra (27 files) - Remove .github/workflows/ci.yml (Gitea shadows .github; runner labels differ) - Fix CI nix-not-found: export /run/current-system/sw/bin in PATH - Trim aarch64 from pre-commit matrix (no QEMU binfmt deployed yet)
This commit is contained in:
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
# NOTE: Caddy handles all TLS natively via its ACME integration.
|
||||
# This module is kept as a fallback for non-Caddy services.
|
||||
# Currently NOT imported in default.nix — uncomment there to activate.
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
{pkgs, ...}: let
|
||||
backupScript = pkgs.writeShellScript "weekly-backup" ''
|
||||
BACKUP_DIR="/home/admin/backups"
|
||||
SRC="/home/admin"
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
domain = "severijnse.eu";
|
||||
{...}: let
|
||||
antiScrape = ''
|
||||
@bad_bot {
|
||||
header_regexp User-Agent "(?i)(scrapy|cpython-requests|python-requests|curl|wget|go-http-client|ltx71|petalbot|bytespider|dotbot|ahrefsbot|semrushbot|mj12bot|dataforseo|facebookexternalhit|claudebot|anthropic-ai|perplexity|gptbot|chatgpt-user|omnisci|imgproxy|ccbot|exabot|360spider|baiduspider|sogou|duckduckgo|amazonbot|cohere-ai|diffbot|imagesiftbot).*"
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
{pkgs, ...}: let
|
||||
zoneFile = pkgs.writeText "severijnse.eu.db" ''
|
||||
$ORIGIN severijnse.eu.
|
||||
$TTL 3600
|
||||
@@ -71,37 +66,48 @@
|
||||
severijnse.eu IN SSHFP 4 2 c02f4b13aef78579f3466851fcc741c0169eaa63237463ceacc9bb72a2be0519
|
||||
'';
|
||||
in {
|
||||
# Decrypt DNSSEC keys from sops-encrypted file using the age key at /etc/age/keys.txt
|
||||
# Uses sops CLI directly instead of sops-nix's sops-install-secrets (avoids Go 1.25 build dep)
|
||||
systemd.services.decrypt-coredns-keys = {
|
||||
description = "Decrypt CoreDNS DNSSEC signing keys";
|
||||
before = ["coredns.service"];
|
||||
wantedBy = ["coredns.service"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
Environment = "SOPS_AGE_KEY_FILE=/etc/age/keys.txt";
|
||||
systemd = {
|
||||
services = {
|
||||
decrypt-coredns-keys = {
|
||||
description = "Decrypt CoreDNS DNSSEC signing keys";
|
||||
before = ["coredns.service"];
|
||||
wantedBy = ["coredns.service"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
Environment = "SOPS_AGE_KEY_FILE=/etc/age/keys.txt";
|
||||
};
|
||||
script = ''
|
||||
SOPS_FILE=${../../secrets/coredns-keys.yaml}
|
||||
KEYS_DIR=/var/lib/coredns/keys
|
||||
mkdir -p "$KEYS_DIR"
|
||||
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_ksk_key"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+20930.key"
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_ksk_private"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+20930.private"
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_zsk_key"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+38678.key"
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_zsk_private"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+38678.private"
|
||||
|
||||
chmod 644 "$KEYS_DIR/"*
|
||||
'';
|
||||
};
|
||||
coredns = {
|
||||
after = ["decrypt-coredns-keys.service"];
|
||||
requires = ["decrypt-coredns-keys.service"];
|
||||
|
||||
# Copy zone file from Nix store to writable location on service start
|
||||
# TLSA updater will modify the writable copy at runtime
|
||||
preStart = ''
|
||||
cp -f ${zoneFile} /var/lib/coredns/zones/severijnse.eu.db
|
||||
chown coredns:coredns /var/lib/coredns/zones/severijnse.eu.db
|
||||
'';
|
||||
};
|
||||
};
|
||||
script = ''
|
||||
SOPS_FILE=${../../secrets/coredns-keys.yaml}
|
||||
KEYS_DIR=/var/lib/coredns/keys
|
||||
mkdir -p "$KEYS_DIR"
|
||||
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_ksk_key"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+20930.key"
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_ksk_private"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+20930.private"
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_zsk_key"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+38678.key"
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_zsk_private"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+38678.private"
|
||||
|
||||
chmod 644 "$KEYS_DIR/"*
|
||||
'';
|
||||
tmpfiles.rules = [
|
||||
"d /var/lib/coredns 0750 coredns coredns -"
|
||||
"d /var/lib/coredns/zones 0750 coredns coredns -"
|
||||
"d /var/lib/coredns/keys 0750 coredns coredns -"
|
||||
];
|
||||
};
|
||||
|
||||
# Ensure coredns waits for key decryption
|
||||
systemd.services.coredns = {
|
||||
after = ["decrypt-coredns-keys.service"];
|
||||
requires = ["decrypt-coredns-keys.service"];
|
||||
};
|
||||
|
||||
services.coredns = {
|
||||
enable = true;
|
||||
config = ''
|
||||
@@ -126,19 +132,4 @@ in {
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
# Copy zone file from Nix store to writable location on service start
|
||||
# TLSA updater will modify the writable copy at runtime
|
||||
systemd.services.coredns = {
|
||||
preStart = ''
|
||||
cp -f ${zoneFile} /var/lib/coredns/zones/severijnse.eu.db
|
||||
chown coredns:coredns /var/lib/coredns/zones/severijnse.eu.db
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/coredns 0750 coredns coredns -"
|
||||
"d /var/lib/coredns/zones 0750 coredns coredns -"
|
||||
"d /var/lib/coredns/keys 0750 coredns coredns -"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
# Keep fail2ban as OCI container to preserve the web UI
|
||||
virtualisation.oci-containers.containers.fail2ban = {
|
||||
image = "crazymax/fail2ban:latest";
|
||||
|
||||
@@ -1,54 +1,87 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
unstablePkgs,
|
||||
...
|
||||
}: {
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_14;
|
||||
ensureDatabases = ["gitea"];
|
||||
ensureUsers = [
|
||||
{
|
||||
services = {
|
||||
postgresql = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_14;
|
||||
ensureDatabases = ["gitea"];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "gitea";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
gitea = {
|
||||
enable = true;
|
||||
package = unstablePkgs.gitea;
|
||||
database = {
|
||||
type = "postgres";
|
||||
name = "gitea";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
user = "gitea";
|
||||
};
|
||||
appName = "Jory's Git";
|
||||
lfs.enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
DOMAIN = "git.severijnse.eu";
|
||||
ROOT_URL = "https://git.severijnse.eu/";
|
||||
HTTP_PORT = 3000;
|
||||
SSH_PORT = 222;
|
||||
SSH_LISTEN_PORT = 2222;
|
||||
START_SSH_SERVER = true;
|
||||
SSH_USER = "git";
|
||||
BUILTIN_SSH_SERVER_USER = "git";
|
||||
LANDING_PAGE = "explore";
|
||||
};
|
||||
service = {
|
||||
DISABLE_REGISTRATION = true;
|
||||
REQUIRE_SIGNIN_VIEW = false;
|
||||
};
|
||||
repository = {
|
||||
DEFAULT_BRANCH = "main";
|
||||
};
|
||||
actions = {
|
||||
ENABLED = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
gitea-actions-runner = {
|
||||
# nixos-24-05's gitea-actions-runner module hardcodes bin/act_runner,
|
||||
# but the current upstream package (1.0.3, matching gitea 1.26) ships
|
||||
# bin/gitea-runner. Wrap it so both names resolve.
|
||||
package = pkgs.runCommand "gitea-actions-runner-wrapped" {} ''
|
||||
mkdir -p $out/bin
|
||||
ln -s ${unstablePkgs.gitea-actions-runner}/bin/gitea-runner $out/bin/act_runner
|
||||
'';
|
||||
instances.default = {
|
||||
enable = true;
|
||||
name = "hetzner";
|
||||
url = "https://git.severijnse.eu";
|
||||
tokenFile = "/var/lib/secrets/gitea-runner-token";
|
||||
labels = ["native:host"];
|
||||
hostPackages = with pkgs; [
|
||||
bash
|
||||
coreutils
|
||||
curl
|
||||
gawk
|
||||
gitMinimal
|
||||
gnused
|
||||
nodejs
|
||||
wget
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Gitea connects to local Postgres via Unix socket (peer auth).
|
||||
# No password needed — the socket is at /run/postgresql by default.
|
||||
# createDatabase = true ensures the DB + user are set up automatically.
|
||||
services.gitea = {
|
||||
enable = true;
|
||||
package = unstablePkgs.gitea;
|
||||
database.type = "postgres";
|
||||
database.name = "gitea";
|
||||
database.user = "gitea";
|
||||
appName = "Jory's Git";
|
||||
lfs.enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
DOMAIN = "git.severijnse.eu";
|
||||
ROOT_URL = "https://git.severijnse.eu/";
|
||||
HTTP_PORT = 3000;
|
||||
SSH_PORT = 222;
|
||||
SSH_LISTEN_PORT = 2222;
|
||||
START_SSH_SERVER = true;
|
||||
SSH_USER = "git";
|
||||
BUILTIN_SSH_SERVER_USER = "git";
|
||||
LANDING_PAGE = "explore";
|
||||
};
|
||||
service = {
|
||||
DISABLE_REGISTRATION = true;
|
||||
REQUIRE_SIGNIN_VIEW = false;
|
||||
};
|
||||
repository = {
|
||||
DEFAULT_BRANCH = "main";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Gitea built-in SSH server: listens on high port 2222 (no privileged-cap needed),
|
||||
# while clone URLs advertise port 222. Firewall redirects 222 -> 2222.
|
||||
@@ -61,4 +94,12 @@
|
||||
${pkgs.nftables}/bin/nft add chain inet gitea-redirect prerouting '{ type nat hook prerouting priority dstnat; }' 2>/dev/null || true
|
||||
${pkgs.nftables}/bin/nft add rule inet gitea-redirect prerouting tcp dport 222 redirect to :2222 2>/dev/null || true
|
||||
'';
|
||||
|
||||
# --- Gitea Actions self-hosted CI runner ---
|
||||
# The native runner only exposes `hostPackages` on PATH (see the list above),
|
||||
# which intentionally omits Nix. CI steps export the host's system Nix
|
||||
# (/run/current-system/sw/bin, i.e. Lix) onto PATH rather than installing a
|
||||
# second Nix client, so the running Lix daemon is used directly.
|
||||
# aarch64 builds run under QEMU user-emulation via boot.binfmt below.
|
||||
boot.binfmt.emulatedSystems = ["aarch64-linux"];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.mailserver = {
|
||||
image = "ghcr.io/docker-mailserver/docker-mailserver:latest";
|
||||
autoStart = true;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers = {
|
||||
hbbr = {
|
||||
image = "rustdesk/rustdesk-server:latest";
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.shkeeper = {
|
||||
image = "vsyshost/shkeeper:2.5.29";
|
||||
autoStart = true;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.snappymail = {
|
||||
image = "djmaze/snappymail:latest";
|
||||
autoStart = true;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
{pkgs, ...}: let
|
||||
# Caddy's canonical certificate storage (XDG data dir). Renewals land here,
|
||||
# owned caddy:caddy 0600 — the mail server's non-root Postfix/Dovecot cannot
|
||||
# read it directly, so we copy it into a world-readable distribution dir.
|
||||
@@ -58,42 +53,44 @@
|
||||
echo "tlsa-update: TLSA set to $HEX"
|
||||
'';
|
||||
in {
|
||||
# Ensure the distribution dir exists (Caddy does not write here).
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${distCertDir} 0755 root root - -"
|
||||
];
|
||||
systemd = {
|
||||
# Ensure the distribution dir exists (Caddy does not write here).
|
||||
tmpfiles.rules = [
|
||||
"d ${distCertDir} 0755 root root - -"
|
||||
];
|
||||
|
||||
systemd.services.tlsa-update = {
|
||||
description = "Sync Caddy TLS certificate to mail server and update DANE/TLSA records";
|
||||
after = ["caddy.service" "coredns.service"];
|
||||
partOf = ["coredns.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = with pkgs; [openssl coreutils gnused podman systemd];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${syncScript}";
|
||||
User = "root";
|
||||
Group = "root";
|
||||
services.tlsa-update = {
|
||||
description = "Sync Caddy TLS certificate to mail server and update DANE/TLSA records";
|
||||
after = ["caddy.service" "coredns.service"];
|
||||
partOf = ["coredns.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = with pkgs; [openssl coreutils gnused podman systemd];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${syncScript}";
|
||||
User = "root";
|
||||
Group = "root";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Fire as soon as Caddy rewrites the certificate on renewal (the atomic rewrite
|
||||
# changes the directory mtime), eliminating the up-to-24h DANE drift window.
|
||||
systemd.paths.tlsa-update = {
|
||||
description = "Watch Caddy certificate directory for renewal";
|
||||
wantedBy = ["paths.target"];
|
||||
pathConfig = {
|
||||
PathModified = [caddyCertDir];
|
||||
Unit = "tlsa-update.service";
|
||||
# Fire as soon as Caddy rewrites the certificate on renewal (the atomic rewrite
|
||||
# changes the directory mtime), eliminating the up-to-24h DANE drift window.
|
||||
paths.tlsa-update = {
|
||||
description = "Watch Caddy certificate directory for renewal";
|
||||
wantedBy = ["paths.target"];
|
||||
pathConfig = {
|
||||
PathModified = [caddyCertDir];
|
||||
Unit = "tlsa-update.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Fallback in case a renewal event is missed (e.g. inotify overflow).
|
||||
systemd.timers.tlsa-update = {
|
||||
wantedBy = ["timers.target"];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
# Fallback in case a renewal event is missed (e.g. inotify overflow).
|
||||
timers.tlsa-update = {
|
||||
wantedBy = ["timers.target"];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.vaultwarden = {
|
||||
image = "vaultwarden/server:latest";
|
||||
autoStart = true;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.watchtower = {
|
||||
image = "ghcr.io/nicholas-fedor/watchtower:latest";
|
||||
autoStart = true;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.wg-easy = {
|
||||
image = "ghcr.io/wg-easy/wg-easy:latest";
|
||||
autoStart = true;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.wrxproxy = {
|
||||
image = "localhost/wrxproxy:latest";
|
||||
autoStart = false;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
{pkgs, ...}: {
|
||||
time.timeZone = "Europe/Amsterdam";
|
||||
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
Reference in New Issue
Block a user