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, ...); 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, inherit, bool-compare guards); add missing trailing newlines
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ gitea.workflow }}-${{ gitea.head_ref || gitea.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
flake-check:
|
||||
runs-on: native
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system: [x86_64-linux, aarch64-linux]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Nix
|
||||
run: |
|
||||
curl -L https://nixos.org/nix/install | sh -s -- --no-daemon
|
||||
mkdir -p "$HOME/.config/nix"
|
||||
echo 'experimental-features = nix-command flakes' >> "$HOME/.config/nix/nix.conf"
|
||||
# shellcheck disable=SC1091
|
||||
source "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
- name: Flake check (${{ matrix.system }})
|
||||
run: |
|
||||
# shellcheck disable=SC1091
|
||||
source "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
nix flake check --no-build --system ${{ matrix.system }}
|
||||
|
||||
pre-commit:
|
||||
runs-on: native
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system: [x86_64-linux, aarch64-linux]
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Nix
|
||||
run: |
|
||||
curl -L https://nixos.org/nix/install | sh -s -- --no-daemon
|
||||
mkdir -p "$HOME/.config/nix"
|
||||
echo 'experimental-features = nix-command flakes' >> "$HOME/.config/nix/nix.conf"
|
||||
# shellcheck disable=SC1091
|
||||
source "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
- name: Pre-commit checks (${{ matrix.system }})
|
||||
run: |
|
||||
# shellcheck disable=SC1091
|
||||
source "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
nix build .#checks.${{ matrix.system }}.pre-commit
|
||||
@@ -45,8 +45,8 @@ jobs:
|
||||
- name: Flake check (${{ matrix.system }})
|
||||
run: nix flake check --no-build --system ${{ matrix.system }}
|
||||
|
||||
format:
|
||||
name: Formatting check (${{ matrix.system }})
|
||||
pre-commit:
|
||||
name: Pre-commit checks (${{ matrix.system }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- x86_64-linux
|
||||
- aarch64-linux
|
||||
runs-on: ${{ matrix.system == 'aarch64-linux' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
@@ -65,7 +65,10 @@ jobs:
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
# alejandra is the configured formatter (flake.nix perSystem.formatter).
|
||||
# Checked per-system so both architectures stay formatted.
|
||||
- name: Formatting check (${{ matrix.system }})
|
||||
run: nix run .#formatter.${{ matrix.system }} -- --check .
|
||||
# Build the pre-commit check derivation: runs alejandra (--check),
|
||||
# statix, deadnix, actionlint and the pre-commit-hooks suite
|
||||
# (trailing-whitespace, end-of-file-fixer, check-yaml/toml,
|
||||
# check-added-large-files, check-merge-conflicts, detect-private-keys)
|
||||
# in a read-only sandbox. Fails the build on any reported problem.
|
||||
- name: Pre-commit checks (${{ matrix.system }})
|
||||
run: nix build .#checks.${{ matrix.system }}.pre-commit
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
Generated
+38
@@ -82,6 +82,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
@@ -122,6 +138,27 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks-nix": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1783008725,
|
||||
"narHash": "sha256-jGiy6+sxjNWXSjp25uoJuNfyH9zBK1PEDY0lVoL4ibQ=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "bca82caa46d5ec0f5d422c61fb1e30bc51313cbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"helium": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -254,6 +291,7 @@
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-parts": "flake-parts",
|
||||
"flake-utils": "flake-utils",
|
||||
"git-hooks-nix": "git-hooks-nix",
|
||||
"helium": "helium",
|
||||
"import-tree": "import-tree",
|
||||
"nix-index-db": "nix-index-db",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
|
||||
systems = ["x86_64-linux" "aarch64-linux"];
|
||||
|
||||
imports = [./hosts ./pkgs];
|
||||
imports = [./hosts ./pkgs inputs.git-hooks-nix.flakeModule];
|
||||
|
||||
perSystem = {
|
||||
config,
|
||||
@@ -21,6 +21,37 @@
|
||||
};
|
||||
# Nix Formatter
|
||||
formatter = pkgs.alejandra;
|
||||
|
||||
# Pre-commit hooks (flake-parts module from cachix/git-hooks.nix).
|
||||
# `nix build .#checks.<system>.pre-commit` runs these in CI; the same
|
||||
# set is installed in `nix develop` for local use.
|
||||
# alejandra.settings.check = true => verify-only (no in-place writes),
|
||||
# which is what we want in the read-only CI sandbox.
|
||||
pre-commit.settings.hooks = {
|
||||
alejandra = {
|
||||
enable = true;
|
||||
settings.check = true;
|
||||
};
|
||||
statix = {
|
||||
enable = true;
|
||||
# hardware-configuration.nix is auto-generated by NixOS; it legitimately
|
||||
# repeats `boot` keys, which statix would otherwise flag. Exclude it here
|
||||
# (the pre-commit statix run does not read the repo-root statix.toml).
|
||||
settings.ignore = ["hardware-configuration.nix"];
|
||||
};
|
||||
# deadnix disabled for now: 36 existing modules declare unused lambda
|
||||
# patterns (e.g. `config`/`lib`/`pkgs`/`inputs` in args). Re-enable once
|
||||
# that cleanup lands so `nix build .#checks.<system>.pre-commit` stays green.
|
||||
deadnix.enable = false;
|
||||
actionlint.enable = true;
|
||||
trim-trailing-whitespace.enable = true;
|
||||
end-of-file-fixer.enable = true;
|
||||
check-yaml.enable = true;
|
||||
check-toml.enable = true;
|
||||
check-added-large-files.enable = true;
|
||||
check-merge-conflicts.enable = true;
|
||||
detect-private-keys.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,6 +71,14 @@
|
||||
inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Nix-native pre-commit framework (maintained successor to
|
||||
# pre-commit-hooks.nix). Follows our nixpkgs to avoid version skew
|
||||
# with the rest of the flake.
|
||||
git-hooks-nix = {
|
||||
url = "github:cachix/git-hooks.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
# rest of inputs, alphabetical order
|
||||
|
||||
@@ -36,10 +36,11 @@
|
||||
(
|
||||
if typeOf element == "null"
|
||||
then "null"
|
||||
else if element == false
|
||||
then "false"
|
||||
else if element == true
|
||||
else if typeOf element == "bool"
|
||||
then
|
||||
if element
|
||||
then "true"
|
||||
else "false"
|
||||
else if typeOf element == "string"
|
||||
then ''"${sanitizeString element}"''
|
||||
else toString element
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
binds = import ./_binds.nix {inherit pkgs;};
|
||||
rules = import ./_rules.nix;
|
||||
|
||||
finalConfig = toKDL.generate "niri-config.kdl" (settings // {binds = binds;} // rules);
|
||||
finalConfig = toKDL.generate "niri-config.kdl" (settings // {inherit binds;} // rules);
|
||||
in {
|
||||
environment.sessionVariables = {
|
||||
NIRI_CONFIG = "$HOME/.config/niri/config.kdl";
|
||||
|
||||
@@ -80,19 +80,21 @@ in {
|
||||
enable = true;
|
||||
defaultApplications = associations;
|
||||
};
|
||||
configFile."xdg-desktop-portal-termfilechooser/config".text = ''
|
||||
configFile = {
|
||||
"xdg-desktop-portal-termfilechooser/config".text = ''
|
||||
[filechooser]
|
||||
cmd=${yaziWrapper}/bin/yazi-wrapper
|
||||
default_dir=$HOME
|
||||
open_mode=suggested
|
||||
save_mode=suggested
|
||||
'';
|
||||
configFile."user-dirs.dirs".source = userDirsConfig;
|
||||
configFile."mimeapps.list".text = ''
|
||||
"user-dirs.dirs".source = userDirsConfig;
|
||||
"mimeapps.list".text = ''
|
||||
[Default Applications]
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k}=${lib.concatStringsSep ";" v}") associations)}
|
||||
[Added Associations]
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k}=${lib.concatStringsSep ";" v}") associations)}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+1
-2
@@ -55,8 +55,7 @@ in {
|
||||
hetzner = nixosSystem24 {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
inherit inputs self;
|
||||
unstablePkgs = unstablePkgs;
|
||||
inherit inputs self unstablePkgs;
|
||||
};
|
||||
modules = [
|
||||
inputs.disko.nixosModules.disko
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -71,9 +71,9 @@
|
||||
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 = {
|
||||
systemd = {
|
||||
services = {
|
||||
decrypt-coredns-keys = {
|
||||
description = "Decrypt CoreDNS DNSSEC signing keys";
|
||||
before = ["coredns.service"];
|
||||
wantedBy = ["coredns.service"];
|
||||
@@ -95,13 +95,24 @@ in {
|
||||
chmod 644 "$KEYS_DIR/"*
|
||||
'';
|
||||
};
|
||||
|
||||
# Ensure coredns waits for key decryption
|
||||
systemd.services.coredns = {
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
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 -"
|
||||
];
|
||||
};
|
||||
services.coredns = {
|
||||
enable = true;
|
||||
config = ''
|
||||
@@ -126,19 +137,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 -"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
unstablePkgs,
|
||||
...
|
||||
}: {
|
||||
services.postgresql = {
|
||||
services = {
|
||||
postgresql = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_14;
|
||||
ensureDatabases = ["gitea"];
|
||||
@@ -17,15 +18,14 @@
|
||||
];
|
||||
};
|
||||
|
||||
# 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 = {
|
||||
gitea = {
|
||||
enable = true;
|
||||
package = unstablePkgs.gitea;
|
||||
database.type = "postgres";
|
||||
database.name = "gitea";
|
||||
database.user = "gitea";
|
||||
database = {
|
||||
type = "postgres";
|
||||
name = "gitea";
|
||||
user = "gitea";
|
||||
};
|
||||
appName = "Jory's Git";
|
||||
lfs.enable = true;
|
||||
settings = {
|
||||
@@ -47,8 +47,43 @@
|
||||
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.
|
||||
|
||||
# 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 +96,10 @@
|
||||
${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 ---
|
||||
# Jobs install their own Nix inside the runner (official installer,
|
||||
# --no-daemon) so no system Nix daemon / nix-users group is needed.
|
||||
# aarch64 builds run under QEMU user-emulation via boot.binfmt below.
|
||||
boot.binfmt.emulatedSystems = ["aarch64-linux"];
|
||||
}
|
||||
|
||||
@@ -58,12 +58,13 @@
|
||||
echo "tlsa-update: TLSA set to $HEX"
|
||||
'';
|
||||
in {
|
||||
systemd = {
|
||||
# Ensure the distribution dir exists (Caddy does not write here).
|
||||
systemd.tmpfiles.rules = [
|
||||
tmpfiles.rules = [
|
||||
"d ${distCertDir} 0755 root root - -"
|
||||
];
|
||||
|
||||
systemd.services.tlsa-update = {
|
||||
services.tlsa-update = {
|
||||
description = "Sync Caddy TLS certificate to mail server and update DANE/TLSA records";
|
||||
after = ["caddy.service" "coredns.service"];
|
||||
partOf = ["coredns.service"];
|
||||
@@ -79,7 +80,7 @@ in {
|
||||
|
||||
# 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 = {
|
||||
paths.tlsa-update = {
|
||||
description = "Watch Caddy certificate directory for renewal";
|
||||
wantedBy = ["paths.target"];
|
||||
pathConfig = {
|
||||
@@ -89,11 +90,12 @@ in {
|
||||
};
|
||||
|
||||
# Fallback in case a renewal event is missed (e.g. inotify overflow).
|
||||
systemd.timers.tlsa-update = {
|
||||
timers.tlsa-update = {
|
||||
wantedBy = ["timers.target"];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Ignore list for statix. `ignore` is a list of file-glob strings; each
|
||||
# entry disables ALL statix checks for matching files (basename match).
|
||||
# hardware-configuration.nix is auto-generated by nixos-generate-config
|
||||
# and intentionally uses repeated top-level keys; it must stay excluded
|
||||
# or a future "fix" breaks regeneration. Keep this entry in sync if more
|
||||
# generated hardware files appear.
|
||||
ignore = [
|
||||
"hardware-configuration.nix",
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
{...}: {
|
||||
_: {
|
||||
systemd.user.services.xdg-desktop-portal = {
|
||||
serviceConfig = {
|
||||
UnsetEnvironment = "NIX_XDG_DESKTOP_PORTAL_DIR";
|
||||
|
||||
Reference in New Issue
Block a user