Compare commits
16 Commits
205a1ccc8f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ac2ca3c8be | |||
| 9f7431bb36 | |||
| b80316267f | |||
| e64c3d1310 | |||
| 75be43140f | |||
| 38bd5b63f5 | |||
| 31de8eccb0 | |||
| cb8835bbfb | |||
| ea79663c96 | |||
| 2e9ea868b6 | |||
| f806506d9a | |||
|
62c70dab19
|
|||
|
4ee5658053
|
|||
|
f489259af9
|
|||
|
0893f80600
|
|||
|
582faec359
|
+7
-25
@@ -13,10 +13,13 @@
|
||||
# PATH before invoking `nix`. This uses the host's actual Lix rather than installing a
|
||||
# second Nix client that would mismatch the running Lix daemon.
|
||||
# * Gitea context vars (gitea.workflow / gitea.head_ref / gitea.sha) for concurrency.
|
||||
# * matrix over x86_64-linux + aarch64-linux for flake-check (--no-build, eval-only).
|
||||
# Pre-commit checks run on x86_64-linux only: building aarch64 derivations needs
|
||||
# QEMU binfmt (registered via boot.binfmt.emulatedSystems) + nix extra-platforms,
|
||||
# which require a nixos-rebuild switch that hasn't been applied yet.
|
||||
# * matrix over x86_64-linux + aarch64-linux.
|
||||
# All jobs use --no-build (eval-only) because the runner has 2 cores / 4GB RAM:
|
||||
# building nix derivations would exhaust memory and crash the server.
|
||||
# Pre-commit hooks run locally via git-hooks-nix on the developer's machine.
|
||||
# * aarch64 builds need QEMU binfmt (boot.binfmt.emulatedSystems) + nix extra-platforms,
|
||||
# which require a nixos-rebuild switch not yet applied. Flake-check for aarch64
|
||||
# runs eval-only (no build) and passes.
|
||||
|
||||
name: CI
|
||||
|
||||
@@ -61,24 +64,3 @@ jobs:
|
||||
run: |
|
||||
export PATH=/run/current-system/sw/bin:$PATH
|
||||
nix flake check --no-build --system ${{ matrix.system }}
|
||||
|
||||
pre-commit:
|
||||
name: Pre-commit checks (${{ matrix.system }})
|
||||
runs-on: native
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# aarch64-linux omitted: building aarch64 derivations needs QEMU binfmt +
|
||||
# extra-platforms; system hasn't been rebuilt to apply them yet.
|
||||
system:
|
||||
- x86_64-linux
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Pre-commit checks (${{ matrix.system }})
|
||||
run: |
|
||||
export PATH=/run/current-system/sw/bin:$PATH
|
||||
nix build .#checks.${{ matrix.system }}.pre-commit
|
||||
|
||||
@@ -148,4 +148,4 @@ Inspired by: [hjem](https://github.com/nix-community/hjem), [owl4ce](https://git
|
||||
|
||||
## License
|
||||
|
||||
MIT — use freely, credits appreciated.
|
||||
MIT — use freely, credits appreciated.
|
||||
|
||||
Generated
+87
-58
@@ -1,52 +1,5 @@
|
||||
{
|
||||
"nodes": {
|
||||
"agenix": {
|
||||
"inputs": {
|
||||
"darwin": "darwin",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": [
|
||||
"systems"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770165109,
|
||||
"narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=",
|
||||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"rev": "b027ee29d959fda4b60b57566d64c98a202e0feb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1744478979,
|
||||
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
|
||||
"owner": "lnl7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "lnl7",
|
||||
"ref": "master",
|
||||
"repo": "nix-darwin",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"disko": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -138,6 +91,24 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks-nix": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
@@ -181,24 +152,24 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"himmelblau": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745494811,
|
||||
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
|
||||
"lastModified": 1783966245,
|
||||
"narHash": "sha256-yFLScy0bxU6GLf4ZUoYCzZiyh5AGTT85evjw6xpG++g=",
|
||||
"owner": "himmelblau-idm",
|
||||
"repo": "himmelblau",
|
||||
"rev": "00b27a924c2a42d54320b9ebc012c22d8dd95e28",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"owner": "himmelblau-idm",
|
||||
"ref": "main",
|
||||
"repo": "himmelblau",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -217,6 +188,27 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"mdatp": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1747306795,
|
||||
"narHash": "sha256-PF4ptTqa3gLzb9ac9SGrFyy4KHd2Z47aUUzkaYmrtvM=",
|
||||
"owner": "epetousis",
|
||||
"repo": "nix-mdatp",
|
||||
"rev": "30936772fbc654604004b7049b727f42a0da6c2f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "epetousis",
|
||||
"repo": "nix-mdatp",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-index-db": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -303,23 +295,45 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"agenix": "agenix",
|
||||
"disko": "disko",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-parts": "flake-parts",
|
||||
"flake-utils": "flake-utils",
|
||||
"git-hooks-nix": "git-hooks-nix",
|
||||
"helium": "helium",
|
||||
"himmelblau": "himmelblau",
|
||||
"import-tree": "import-tree",
|
||||
"mdatp": "mdatp",
|
||||
"nix-index-db": "nix-index-db",
|
||||
"nixos-24-05": "nixos-24-05",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"rustlock": "rustlock",
|
||||
"sops-nix": "sops-nix",
|
||||
"systems": "systems_2",
|
||||
"systems": "systems_3",
|
||||
"zen-browser": "zen-browser"
|
||||
}
|
||||
},
|
||||
"rustlock": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782663473,
|
||||
"narHash": "sha256-wPpiuL3EVvNb3+9QgRGtopZXbebb9uUs3Slwuigvh8w=",
|
||||
"owner": "JorySeverijnse",
|
||||
"repo": "rustlock",
|
||||
"rev": "76039750623825172fbee93adae8bb48d4ad36ad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "JorySeverijnse",
|
||||
"repo": "rustlock",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"sops-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -356,6 +370,21 @@
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
|
||||
@@ -82,14 +82,6 @@
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
# rest of inputs, alphabetical order
|
||||
agenix = {
|
||||
url = "github:ryantm/agenix";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
systems.follows = "systems";
|
||||
};
|
||||
};
|
||||
|
||||
import-tree.url = "github:vic/import-tree";
|
||||
|
||||
zen-browser = {
|
||||
@@ -102,6 +94,21 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
himmelblau = {
|
||||
url = "github:himmelblau-idm/himmelblau/main";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
mdatp = {
|
||||
url = "github:epetousis/nix-mdatp";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
rustlock = {
|
||||
url = "github:JorySeverijnse/rustlock";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nix-index-db = {
|
||||
url = "github:Mic92/nix-index-database";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"Mod+B".spawn._args = ["zen"];
|
||||
"Mod+Return".spawn._args = ["alacritty"];
|
||||
"Mod+Q".close-window = {};
|
||||
"Mod+Shift+P".spawn._args = ["rofi-powermenu"];
|
||||
"Mod+Shift+P".spawn._args = ["rustlock" "--screenshots" "--clock" "--effect-blur" "7x5" "--effect-vignette" "0.5:0.5"];
|
||||
"Mod+S".switch-preset-column-width = {};
|
||||
"Mod+F".fullscreen-window = {};
|
||||
"Mod+1".set-column-width = "25%";
|
||||
|
||||
@@ -10,34 +10,34 @@
|
||||
|
||||
# Hook that auto-generates a Conventional Commit message from staged diff
|
||||
prepareCommitMsg = pkgs.writeShellScript "prepare-commit-msg" ''
|
||||
# Only run if there are staged changes
|
||||
if git diff --cached --quiet 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
# Only run if there are staged changes
|
||||
if git diff --cached --quiet 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Generating commit message from staged changes..." >&2
|
||||
echo "Generating commit message from staged changes..." >&2
|
||||
|
||||
# Generate Conventional Commit message using opencode run
|
||||
git diff --cached | timeout 30 ${pkgs.opencode}/bin/opencode run \
|
||||
-m opencode/north-mini-code-free \
|
||||
"You are an expert software engineer writing professional Git commit messages.
|
||||
# Generate Conventional Commit message using opencode run
|
||||
git diff --cached | timeout 30 ${pkgs.opencode}/bin/opencode run \
|
||||
-m opencode/north-mini-code-free \
|
||||
"You are an expert software engineer writing professional Git commit messages.
|
||||
|
||||
Create a clean Conventional Commit for the provided diff.
|
||||
Create a clean Conventional Commit for the provided diff.
|
||||
|
||||
Rules:
|
||||
- Format: type(optional scope): imperative description
|
||||
- Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
- First line: maximum 72 characters, starts with capital letter, imperative present tense
|
||||
- If relevant, add a blank line followed by a short body explaining the motivation and key changes
|
||||
- Be concise and professional. No emojis, no markdown.
|
||||
Rules:
|
||||
- Format: type(optional scope): imperative description
|
||||
- Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
- First line: maximum 72 characters, starts with capital letter, imperative present tense
|
||||
- If relevant, add a blank line followed by a short body explaining the motivation and key changes
|
||||
- Be concise and professional. No emojis, no markdown.
|
||||
|
||||
Output ONLY the commit message. Do not add any extra text, quotes, or explanations." \
|
||||
2>/dev/null > "$1"
|
||||
Output ONLY the commit message. Do not add any extra text, quotes, or explanations." \
|
||||
2>/dev/null > "$1"
|
||||
|
||||
# Fallback if generation failed or timed out
|
||||
if [ ! -s "$1" ]; then
|
||||
echo "chore: auto-generated commit message" > "$1"
|
||||
fi
|
||||
# Fallback if generation failed or timed out
|
||||
if [ ! -s "$1" ]; then
|
||||
echo "chore: auto-generated commit message" > "$1"
|
||||
fi
|
||||
'';
|
||||
in {
|
||||
users.users.someone.packages = with pkgs; [
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
users.users.someone.packages = with pkgs; [
|
||||
openssh
|
||||
];
|
||||
|
||||
xdg.configFile."ssh/config" = {
|
||||
source = config.sops.secrets.ssh_config.path;
|
||||
};
|
||||
home.file.".ssh/config".text = ''
|
||||
AddKeysToAgent yes
|
||||
CheckHostIP yes
|
||||
Compression no
|
||||
ControlMaster no
|
||||
ControlPath ~/.ssh/master-%r@%n:%p
|
||||
ControlPersist no
|
||||
HashKnownHosts yes
|
||||
IdentitiesOnly yes
|
||||
PasswordAuthentication no
|
||||
ForwardAgent no
|
||||
ForwardX11 no
|
||||
ForwardX11Trusted no
|
||||
ServerAliveInterval 60
|
||||
ServerAliveCountMax 3
|
||||
UserKnownHostsFile ~/.ssh/known_hosts
|
||||
|
||||
home.file.".ssh/config" = {
|
||||
source = config.xdg.configHome + "/ssh/config";
|
||||
mutable = false;
|
||||
};
|
||||
Host git.severijnse.eu
|
||||
HostName git.severijnse.eu
|
||||
Port 222
|
||||
User git
|
||||
IdentityFile /run/secrets/gitea_laptop
|
||||
|
||||
environment.sessionVariables = {
|
||||
SSH_AUTH_SOCK = "${config.xdg.runtimeDir}/gnupg/S.gpg-agent.ssh";
|
||||
SSH_CONFIG = "${config.xdg.configHome}/ssh/config";
|
||||
};
|
||||
Host hetzner
|
||||
HostName severijnse.eu
|
||||
User admin
|
||||
IdentityFile /run/secrets/hetzner_server
|
||||
|
||||
Host github.com
|
||||
HostName github.com
|
||||
User git
|
||||
IdentityFile /run/secrets/github_laptop
|
||||
|
||||
Include ~/.ssh/config.d/*
|
||||
'';
|
||||
|
||||
systemd.user.services.ssh-agent = {
|
||||
description = "SSH agent service";
|
||||
|
||||
+32
-28
@@ -1,8 +1,8 @@
|
||||
{
|
||||
config,
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
@@ -168,40 +168,44 @@
|
||||
|
||||
boot.loader.limine.bootMode = lib.mkDefault "bios";
|
||||
|
||||
# sops-nix configuration
|
||||
# sops-nix configuration — uses SSH host keys automatically, no age key file needed
|
||||
sops = {
|
||||
defaultSopsFile = ./../../secrets/secrets.yaml;
|
||||
age.keyFile = "/etc/age/keys.txt";
|
||||
# Explicitly define secrets to ensure they're generated
|
||||
secrets = {
|
||||
discordo = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
openrouter = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
github = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
twt = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
gemini = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
context7 = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
exa = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
ssh_config = { owner = "someone"; group = "users"; mode = "0440"; };
|
||||
gitea_laptop = { owner = "someone"; group = "users"; mode = "0400"; };
|
||||
hetzner_server = { owner = "someone"; group = "users"; mode = "0400"; };
|
||||
gitea_laptop = {
|
||||
owner = "someone";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
github_laptop = {
|
||||
owner = "someone";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
hetzner_server = {
|
||||
owner = "someone";
|
||||
group = "users";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [pkgs.cryptsetup pkgs.age pkgs.nixd pkgs.apparmor-parser pkgs.xdg-desktop-portal-wlr];
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# Provide age private key for sops decryption at activation time
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /etc/age 0700 root root -"
|
||||
"w /etc/age/keys.txt 0600 root root - # created by activation script"
|
||||
nixpkgs.overlays = [
|
||||
(_self: super: {
|
||||
cisco-secure-client = super.callPackage ../../pkgs/cisco-secure-client {};
|
||||
})
|
||||
];
|
||||
|
||||
# Activation script to write age key
|
||||
system.activationScripts.write-age-key = ''
|
||||
cat > /etc/age/keys.txt << 'AGEEOF'
|
||||
# created: 2026-07-12T10:20:38+02:00
|
||||
# public key: age1n2kvnwdzrd5vw70wwmm90a4lg6wj3665egzpnz72r98epr0rhc0q3yp356
|
||||
AGE-SECRET-KEY-1A49GYAG5AJWGW0XM7HWC8QFE6UUF0S3Z99T252SKX2JHW6NM0HDQESWAQC
|
||||
AGEEOF
|
||||
chmod 600 /etc/age/keys.txt
|
||||
'';
|
||||
security.pam.services.rustlock.text = "auth include login";
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
cryptsetup
|
||||
age
|
||||
nixd
|
||||
apparmor-parser
|
||||
xdg-desktop-portal-wlr
|
||||
inputs.rustlock.packages.${pkgs.stdenv.hostPlatform.system}.default
|
||||
];
|
||||
}
|
||||
|
||||
@@ -29,4 +29,4 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -5,7 +5,6 @@
|
||||
}: let
|
||||
# shorten paths
|
||||
inherit (inputs.nixpkgs.lib) nixosSystem;
|
||||
lib = inputs.nixpkgs.lib;
|
||||
# Server uses its own pinned 24.05 nixpkgs (kept isolated from the laptop's unstable)
|
||||
nixosSystem24 = inputs.nixos-24-05.lib.nixosSystem;
|
||||
unstablePkgs = import inputs.nixpkgs-unstable {system = "x86_64-linux";};
|
||||
@@ -18,12 +17,13 @@
|
||||
# get these into the module system
|
||||
specialArgs = {inherit inputs self;};
|
||||
|
||||
# shared modules for all configurations
|
||||
# shared modules for all configurations (personal + work)
|
||||
sharedModules = [
|
||||
./aesthetic
|
||||
"${mod}/services/gnome-services.nix"
|
||||
"${mod}/core/limine.nix"
|
||||
"${home}"
|
||||
"${self}/work"
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
];
|
||||
in {
|
||||
@@ -59,7 +59,7 @@ in {
|
||||
++ sharedModules
|
||||
++ [
|
||||
"${mod}/services/location.nix"
|
||||
({ config, ... }: {
|
||||
({...}: {
|
||||
boot.loader.limine.bootMode = "uefi";
|
||||
})
|
||||
];
|
||||
@@ -73,13 +73,29 @@ in {
|
||||
++ sharedModules
|
||||
++ [
|
||||
"${mod}/services/location.nix"
|
||||
({ config, ... }: {
|
||||
({...}: {
|
||||
boot.loader.limine.bootMode = "bios";
|
||||
boot.loader.limine.biosDevice = "/dev/nvme0n1";
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
# Laptop work: laptop profile + work modules (himmelblau, cisco, mdatp)
|
||||
laptop-work = nixosSystem {
|
||||
inherit specialArgs;
|
||||
modules =
|
||||
laptop
|
||||
++ sharedModules
|
||||
++ [
|
||||
"${mod}/services/location.nix"
|
||||
{
|
||||
work.cisco.enable = true;
|
||||
work.himmelblau.enable = true;
|
||||
work.mdatp.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Server: severijnse.eu (Hetzner) — fully isolated under servers/hetzner/.
|
||||
# Uses nixos-24.05 + disko + sops-nix and does NOT inherit the laptop's shared modules.
|
||||
hetzner = nixosSystem24 {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
fetchurl,
|
||||
dpkg,
|
||||
makeWrapper,
|
||||
patchelf,
|
||||
coreutils,
|
||||
systemd,
|
||||
glib,
|
||||
zlib,
|
||||
xz,
|
||||
curl,
|
||||
libxml2,
|
||||
}: let
|
||||
version = "5.1.17.3394";
|
||||
# libxml2's "out" output has lib/libxml2.so.16
|
||||
libxml2_out = libxml2.out;
|
||||
|
||||
# System library RPATH for all bundled ELF binaries (cisco's own lib dir is
|
||||
# appended during fixupPhase via $out)
|
||||
sysRpath = lib.makeLibraryPath [
|
||||
systemd
|
||||
glib
|
||||
zlib
|
||||
xz
|
||||
stdenv.cc.cc.lib
|
||||
curl
|
||||
libxml2_out
|
||||
];
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "cisco-secure-client";
|
||||
inherit version;
|
||||
|
||||
src = fetchurl {
|
||||
url = "https://archive.org/download/cisco-secure-client-linux64-${version}/cisco-secure-client-linux64-${version}-predeploy-deb-k9.tgz";
|
||||
sha256 = "5c4cafb4694e64cbf041481f5df3d70389399926f8aa2a469d480c0555b58c2c";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [dpkg makeWrapper patchelf];
|
||||
|
||||
buildInputs = [
|
||||
systemd
|
||||
glib
|
||||
zlib
|
||||
xz
|
||||
stdenv.cc.cc.lib
|
||||
curl
|
||||
libxml2_out
|
||||
];
|
||||
|
||||
dontStrip = true;
|
||||
dontAutoPatchelf = true;
|
||||
|
||||
unpackPhase = ''
|
||||
tar xzf "$src"
|
||||
DEB_FILE=$(ls cisco-secure-client-vpn-cli_*_amd64.deb 2>/dev/null || true)
|
||||
if [ -z "$DEB_FILE" ]; then
|
||||
DEB_FILE=$(ls cisco-secure-client-vpn_*_amd64.deb 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$DEB_FILE" ]; then
|
||||
echo "ERROR: No .deb file found"
|
||||
exit 1
|
||||
fi
|
||||
dpkg-deb -x "$DEB_FILE" .
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p "$out"
|
||||
cp -r opt/* "$out/"
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
# fixupPhase: patchelf corrupts Cisco's embedded code signatures on ALL
|
||||
# shipped ELF binaries and .so plugins. We skip patchelf entirely and
|
||||
# instead rely on LD_LIBRARY_PATH wrappers for system library resolution.
|
||||
# The binaries' original RPATH of /opt/cisco/secureclient/lib resolves
|
||||
# correctly at runtime via the /opt/cisco/secureclient -> store symlink.
|
||||
fixupPhase = ''
|
||||
runHook preFixup
|
||||
|
||||
# Fix absolute symlinks — the deb assumes install under /opt/cisco/secureclient/
|
||||
# but nix puts it in the store. Convert to relative symlinks.
|
||||
for link in $(find "$out" -type l); do
|
||||
target=$(readlink "$link")
|
||||
if echo "$target" | grep -q "^/opt/"; then
|
||||
rel=$(basename "$target")
|
||||
ln -sf "$rel" "$link"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in "$out"/cisco/secureclient/bin/* "$out"/cisco/secureclient/lib/*.so*; do
|
||||
chmod +x "$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Create libxml2.so.2 symlink (SONAME mismatch: Cisco wants .2, nixpkgs provides .16)
|
||||
libxml2_so=$(find ${libxml2_out}/lib -name "libxml2.so.16*" 2>/dev/null | head -1)
|
||||
if [ -n "$libxml2_so" ]; then
|
||||
ln -sf "$libxml2_so" "$out/cisco/secureclient/lib/libxml2.so.2"
|
||||
fi
|
||||
|
||||
# NO patchelf on Cisco ELFs — their embedded code signatures are
|
||||
# verified at runtime (especially plugins loaded by vpnagentd).
|
||||
# All ELFs keep their original RPATH /opt/cisco/secureclient/lib
|
||||
# which resolves via the tmpfiles symlink.
|
||||
# System libs are provided via LD_LIBRARY_PATH in wrappers below.
|
||||
|
||||
ldPath="$out/cisco/secureclient/lib:${sysRpath}"
|
||||
|
||||
# Wrap vpn CLI — wrapProgram renames the original to .vpn-wrapped
|
||||
wrapProgram "$out/cisco/secureclient/bin/vpn" \
|
||||
--prefix LD_LIBRARY_PATH : "$ldPath" \
|
||||
--prefix PATH : ${lib.makeBinPath [coreutils]}
|
||||
|
||||
# Create $out/bin/ wrappers for all user-facing executables
|
||||
mkdir -p "$out/bin"
|
||||
|
||||
makeWrapper "$out/cisco/secureclient/bin/vpn" "$out/bin/vpn" \
|
||||
--prefix LD_LIBRARY_PATH : "$ldPath" \
|
||||
--prefix PATH : ${lib.makeBinPath [coreutils]}
|
||||
|
||||
makeWrapper "$out/cisco/secureclient/bin/vpnagentd" "$out/bin/vpnagentd" \
|
||||
--prefix LD_LIBRARY_PATH : "$ldPath" \
|
||||
--prefix PATH : ${lib.makeBinPath [coreutils]}
|
||||
|
||||
for cli in acinstallhelper manifesttool_vpn vpndownloader-cli; do
|
||||
if [ -f "$out/cisco/secureclient/bin/$cli" ]; then
|
||||
makeWrapper "$out/cisco/secureclient/bin/$cli" "$out/bin/$cli" \
|
||||
--prefix LD_LIBRARY_PATH : "$ldPath"
|
||||
fi
|
||||
done
|
||||
|
||||
runHook postFixup
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Cisco Secure Client (AnyConnect successor) VPN client";
|
||||
homepage = "https://www.cisco.com/site/us/en/products/security/secure-client/index.html";
|
||||
sourceProvenance = with sourceTypes; [binaryNativeCode];
|
||||
license = {
|
||||
name = "Cisco Secure Client EULA — proprietary, not redistributable";
|
||||
url = "https://www.cisco.com/c/en/us/products/security/secure-client/eula.html";
|
||||
};
|
||||
platforms = ["x86_64-linux"];
|
||||
maintainers = [];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/nix/store/p2h0fr7k47yrx3x0qkr3rwsp5nf5bj30-nixos-system-debian-4gb-fsn1-1-24.05.20241230.b134951
|
||||
@@ -1,3 +0,0 @@
|
||||
# created: 2026-07-12T10:20:38+02:00
|
||||
# public key: age1n2kvnwdzrd5vw70wwmm90a4lg6wj3665egzpnz72r98epr0rhc0q3yp356
|
||||
AGE-SECRET-KEY-1A49GYAG5AJWGW0XM7HWC8QFE6UUF0S3Z99T252SKX2JHW6NM0HDQESWAQC
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
age-encryption.org/v1
|
||||
-> ssh-ed25519 o3KszQ O4xOtTDK/DH+r81x55jFRFtPAFpTpvCiUcUBy/bm320
|
||||
K8kjoDS2ODYJdNZCUmhTfQOWFHEFCkb90d9K5fEnxYo
|
||||
--- j1a/Uo7QXWC0dnbSGKaRj/T9GkChC7BQf6kYC0HN8f4
|
||||
êßçé}N
|
||||
,âôxoT#c„d¸b,¥Á›�o¶wšah@Z˜&àWU]S6T �þôá”ó¾þº)Yõg
|
||||
@@ -1,22 +0,0 @@
|
||||
let
|
||||
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICL0B9moxOPY4XYim8826M+Tf0sWErwWljz1gkDZPwuF";
|
||||
in {
|
||||
"gitea_laptop.age".publicKeys = [
|
||||
host
|
||||
];
|
||||
"hetzner_server.age".publicKeys = [
|
||||
host
|
||||
];
|
||||
"aur.age".publicKeys = [
|
||||
host
|
||||
];
|
||||
"ssh_config.age".publicKeys = [
|
||||
host
|
||||
];
|
||||
"github_key.age".publicKeys = [
|
||||
host
|
||||
];
|
||||
"sporestack_server.age".publicKeys = [
|
||||
host
|
||||
];
|
||||
}
|
||||
+13
-20
@@ -1,25 +1,18 @@
|
||||
discordo: ""
|
||||
openrouter: ""
|
||||
github: ""
|
||||
twt: ""
|
||||
gemini: ""
|
||||
context7: ""
|
||||
exa: ""
|
||||
gitea_laptop: ""
|
||||
ssh_config: ENC[AES256_GCM,data:TqAUIElmt9nmVC9GuPoo+4/K1VSjxyJdqKe2ra38cJjC66aozIBk5dHyJPgMai/V7jPfYGk366cQUmkE1cvzz/DCw19Xa06YPM2qr93g0FOq0YFu33Vn+33Sx5UAM2QeaTHlIOqqZDBYhJpiOH/5WwCZ/Ai2qInj2G5lH28FiJU5frpImqY2Sq0E6Mhwt3++4+tAcaam+jp1V+PfdcroCULOjpJkbIpNx34TPZPmAtEN/fkkKr6NLj1Jh4RnmdMSW20FHzKSeDKIuDbFdUODRd+jRGo1G6DyujrWV10JrggxR24cfXA15C4nFaEZ6ZHshU1IXoq2msalGqaAHhLetaJaybsWTVpbpkMEozswrkmHiGSHHmq1b6A15i3QVApvFkT02D2Rihs4i3wTPav3zsPdUEUVTGOKI/VQcbJIfckZLZLxLUQjpX+Y62pbsIjtdfF6c/PvCSp5B0+KSZOGvOmZj/RvpDLLeeg5l0tSz5HBaaNPC5LHNuI9Fjabja6EjLhtmZdffOF0yyHjau0AwYYelyjao87gXSz9SuAt5NBvvkB6QifvLJZS6PaNl38OMkDvl1W5n89S8bJVZfmV/1nDZqDdpAPidn0AtT+iKP8As6tuTWnRKATALNqFKMpac3NOAuDWxzB2awhdGBFOJlwjTeepUVZrAbk9sqr9TsNXv8Kr8CyAW59ZL3GmbEik6JN0yuWJioHL4G6TiYbkAK4iIfjhnoyyyUtQhw==,iv:UsyEPs3PbY4rqS2ovBsUUv50ksVUp6UJyKn39i6G5O8=,tag:7s0iZKcLtlUiP7AIP+d5dA==,type:str]
|
||||
hetzner_server: ENC[AES256_GCM,data:Cb/oa7oDO6nsbxEQmqYHqPP6AEqUHQzHc49axXPJWOkvOb4WMAMh93xVdFzdgPIR1MjLusrD0q2azE6gEQtT+qPbhB0A0/RN/DR49U04UGzWAPltDGlJX+0Z01CL34Kvy6lzOeaVgWLD5Uzg628uGNqL3PKHceJI6Q9lWbH9E19jBGOq86ceXerVlzwDQNyLyrT75O3zOXFy3V0OmG0WDi6pI61ReZVx5HZe3nmxkAMTsRjs+FxA/Yr6hZa0QVqczoZ+i/+qI3RBrWxqUko/A6CGWM/PzMkhw9FCVRoMwRANKoaom2MX+DSDIbg1Bg2JvhUR9oxtNRpO9mj0R6wLlFzA31h9tle0q+7pNxW68G9YDhWUFcxcvb/xFjpSJ7C1kfGQJADpLJoIgT+Fo1XvyiyRKwIXDN2wvAGoCF6gtZcVesN/xhMbmLHPOedHdNrUbSNhZGvR4/mzSlmSLkixddI+r9cXbH/VH+rXd0cCOaqcMEv2FmSRW9z1/y9dPZrpdwg88oouSNzVIE0ANxoARyF8x7mEGNwGHBG1,iv:AreeGiJvChbJDqBdR1L0c/7aqqrfcsQ7ApBpg+0hYOQ=,tag:1lnO8QlFad56basn0NpYmg==,type:str]
|
||||
gitea_laptop: ENC[AES256_GCM,data:nfiUKIe/+4CuHcv9oBNRFL+gGx2XEvjZlllOVhQYKd00rlffCh1k2BHChdXYziaPVo7SvFkZUX1OLLYqtKC90qMocfSq+6j7hJiTO7RH+DUaAzUEY1bANd6mwh2gl63Xcq3Z65VFRnYvXigJS3QZGykeXNancXjs3TK5pjJZDXgXef7gKzs7yLlnr41Ntk2eiOv08334aa046Qjr6kDjtGd/uUkTzuetMPfGadr/I0VBf0S/SjwSRSzM12CTa7vIz+AbY4pOrZYV+s0bHtuVfE0Xv9WqjC2rA9rqhzrcVfzdbhIEIpADoyhezImcZTzUFQFgaPRoH6ko8GhZKUXPRAmLeMvuzHy1y4kXjsfSixMM2doEfh2lKJTAf1DtAt4OrzXLYt+n5gxCuXdcIMHGP/wfbfmC+Lb1iPZ1ywzICBmBXugNw1KoIMnnr/DjRdqjNQiGQtEd8ylu3VpK7a5OHb0/kW6YQCvtPy5xgYn19LYJUIp8xb4bA2Ty2Bi0FbnfDQbtejUK1bfgz14SQXK6,iv:O+8iWBbXSiyagkfRjzYDxMukBZHfCgxxsEx6C1OBjjQ=,tag:KyzATKtgjutfDmHjR5Gq9g==,type:str]
|
||||
github_laptop: ENC[AES256_GCM,data:sIiuxSVQyxNIM1BDXx/Wjtdjl7CS9Hdq8tQu9Qdu9fcoDVwNpuidflhb7K7jXmynLJDcAgqSJIBuVngU6mf7nA/k/EmWc8T/+Pl8RUqR/Z85F0Bby/PEULf8pljxiqbolm9fIe9ayThxFJux6tq+cfXiN/omHWxJuMNWgroX6xoxmBla/TDI8VdO1z60wogAJDd0XWp766tFX11iXMuTJhFUKO4Ah3CCHTuIvmXhfMrqdykrulbRgkhoOl/ufNZnJA0cMeg3hjb/yZU29TNaKHXZyeIbP2B/dvEJjNgBQnVoqEL3K4ZZOp4Bo52pdw+wLQ9bQY1hN/XYwi5qNC6G5A0i9r3tmYGeR+6lHIWGaQ0LbFiGmnLQGIeYOU0b7CweJFqJzBDkmfPnglnsRNJtVb7UgBJYGFcLPS0XfVYUVjN0OTqUugeTJSUCAMnWnBlsIgWbrclwe9ilNRPELf7bqXC+Lfcl5vgH1JJRB1Q8m//v+2j764m04Oo4uVZRHqkhA0Jdq/58E2NQpBSM1UNY,iv:cFyGuGa9dWITfJY5rvYe6WIZDouU1ZQ1Lj5x9mU/9wc=,tag:dH5XDlbi4LE8uyAVDMT6dw==,type:str]
|
||||
hetzner_server: ENC[AES256_GCM,data:YW3eiBJ+4hqBY3BVIcoJicSRz2L+9iZzwVdhpRl/BARtwkkPEohRUUIXY15HqS/DqhcKaojh1YqlUzFh5Z+FKTnizhPNHwWN+Dnz4d9dNTyIdzRkKOp31uYTJi2v2uwoYc60y4wkhHO5dAvWKdHz2qdvXCS9uspfaVpc2k/HasnBmpUODZSfUnLq2bInifDf+lPuJqTVAH0QXo93Nx6IqbgOA0WLfdi6xC3PemzwtqL6dhXUPehF3N4aULfbph/JHnxqQ3LqvDnr8wQsqBfePpFfxpod0rtYnYOwQ/TDEErLIG8d7Idvlzgmr8WFBpgYQuoMyYuDr9PYenpNnbeIF3JesCocsBEw4jpoQT7K1Pd0iVTuvp/41X7gPmYrPviVbWx9X99FeHzUDxhOcNSc/QZ4UtPoLMprlmPK2XQPbQBJyy9KXGNMBW/jERE7Et0cmlx/TVA7tN8T6/Y2vrBlgtmE3m6Fn7ZhtiNFZs4GMbK9KGtmgxwN75yJML3x77Wsleco6nrBcYEghf3LoviwQVjw9KKgwSVLJzW+,iv:rhp4eqmAzmQOAtPy9+ATqth5IKKzk3evJyVw5UtDrSc=,tag:TP/2plD04GU9zKfpesLqeA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age1vtlutzzhgsy4ef8jd97jaay30zwygj68jwgn0zx64m2d5u57sqzsngy7lf
|
||||
enc: |
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1WUlMWE93cGJxbDY4VzNB
|
||||
VkVMMXFqZTVXaTFXeFNWdHNJUjV2OE84YVZFClhhK0tUQVlSUXF2NWJFVHFKQXF2
|
||||
VUJ5d0t6Um43bmQ5RmExd1QzZ1BxN0EKLS0tIGNnWlZwUkhxbndSRkxia0dFVjVB
|
||||
TnNEOFBIbU5kSWRCSERQR2hPWnJQM1EKvGPbq3jltkJXL6ExowUeuZ6/WYaBuojX
|
||||
XzUaNchtd22IhWB50V4IDyl79gb4y0felRS4qe5LQvnvXnjCTwoqyQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYVGxBMmhqdDBmRjlSY2Jp
|
||||
TVlFKzc3dnM4YUwzZm1iTXY0Q1ROazZQTWc4CjZQZzRIeFYxaGlrSmZTVEFNb21T
|
||||
M2xGN1ozaEdOak5kQTRYRENXZFpNRmcKLS0tIFhCNHVWejFpeGtCL1cwNi9lU0pq
|
||||
dzJLbGRzZTlnZVAra2x0SXAvclcrelkKZdnhAPvrqixSbUNfn1Tj7ZYaeCUxfZnf
|
||||
T9KxEukEKRXMPfCANiQvlG3bM6pPM7L2l8TjTzWOSN/M31cgbiLjyg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-07-12T08:59:50Z"
|
||||
mac: ENC[AES256_GCM,data:3faasx6fkL6wv9oDDXlxlcbvGIj1D9l2FpkDhYHhyCjN8ZZeRCX4qznos5eLMnDAQUhUZgyvCCXidlHJsYUZcj11p7T0YcGnr+6lzi/p9VkETA2v42BlgXgz0M9nLY0mB6kbAY8JhaJ5fO59DDEhSZRKNgIvzFv2Rv/SoyjpV10=,iv:J1g5R43LOKMSy0V0/cO8eUiuF52ML+2G8gp1WZeOFNM=,tag:PobaSjf5ZPVcNDYmaX02vA==,type:str]
|
||||
recipient: age1vtlutzzhgsy4ef8jd97jaay30zwygj68jwgn0zx64m2d5u57sqzsngy7lf
|
||||
lastmodified: "2026-07-12T19:19:12Z"
|
||||
mac: ENC[AES256_GCM,data:iF2a+wQXiWw4vqOedlZyWda3x66lonwh6WFcB5DdZyXbZzxq8Nvz4POGVYS4ZDwDlWrDhS9RTCX/UyclrHxBslu+rkSUqnOITOABWlnwFPTjGRz4EVMk1MGpRHXL9c8N2O1SS0LnkL8O6+9qsIROkNQVi4nyo4zIYTEfcdxDx+w=,iv:qPf6pl8d4egPitSCheccfiek+WPJVx33cZe81HggBio=,tag:4zXI+g6+vPo0YWOoyud8vw==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.12.2
|
||||
version: 3.13.2
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -12,15 +12,22 @@
|
||||
../../modules/services/wireguard.nix
|
||||
../../modules/services/fail2ban.nix
|
||||
../../modules/services/vaultwarden.nix
|
||||
../../modules/services/mailserver.nix
|
||||
../../modules/services/snappymail.nix
|
||||
../../modules/services/rustdesk.nix
|
||||
../../modules/services/wrxproxy.nix
|
||||
../../modules/services/watchtower.nix
|
||||
../../modules/services/tlsa-updater.nix
|
||||
../../modules/services/shkeeper.nix
|
||||
../../modules/services/backup.nix
|
||||
../../modules/services/stalwart.nix
|
||||
../../modules/services/virtualcam.nix
|
||||
../../modules/services/netdata.nix
|
||||
];
|
||||
|
||||
# Only 4GB RAM — limit nix builds to one core at a time to avoid OOM
|
||||
nix.settings = {
|
||||
cores = 1;
|
||||
max-jobs = 1;
|
||||
};
|
||||
|
||||
system.stateVersion = "24.05";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This is the authoritative DNS zone served by CoreDNS
|
||||
# Update serial number on changes
|
||||
{
|
||||
serial = "2026071003";
|
||||
serial = "2026071004";
|
||||
adminEmail = "abuse.severijnse.eu";
|
||||
nameservers = ["ns1.severijnse.eu" "ns2.severijnse.eu"];
|
||||
ipv4 = "49.13.92.205";
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
swapDevices = [
|
||||
{
|
||||
device = "/swap";
|
||||
size = 2048;
|
||||
size = 4096;
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,41 +1,93 @@
|
||||
{pkgs, ...}: let
|
||||
backupScript = pkgs.writeShellScript "weekly-backup" ''
|
||||
BACKUP_DIR="/home/admin/backups"
|
||||
SRC="/home/admin"
|
||||
DATE=$(date +%Y-%m-%dT%H-%M-%S)
|
||||
FILENAME="weekly-backup-$DATE.tar.gz"
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
# sops-encrypted secrets (single file holds all service secrets).
|
||||
secretsFile = ../../secrets/secrets.yaml;
|
||||
# Root-only runtime files restic reads from (0600 root).
|
||||
runtimeDir = "/var/lib/restic";
|
||||
passwordFile = "/var/lib/restic/.password";
|
||||
environmentFile = "/var/lib/restic/environment";
|
||||
# Backblaze B2 backend, per restic docs: b2:bucketname.
|
||||
repo = "b2:hetzner-severijnse";
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
# Backup everything under /home/admin EXCEPT:
|
||||
# - The backups dir itself (infinite loop)
|
||||
# - DMS mail data (GBs of email, backed up separately)
|
||||
# - NixOS-managed service data (at their own paths below)
|
||||
tar czf "$BACKUP_DIR/$FILENAME" \
|
||||
--exclude="$BACKUP_DIR" \
|
||||
--exclude="/home/admin/backups" \
|
||||
--exclude="/home/admin/dms/mail-data" \
|
||||
--exclude="/home/admin/dms/mail-state" \
|
||||
"$SRC"
|
||||
# Materialize the restic password and B2 credentials from sops into
|
||||
# root-only files, so secrets are never world-readable in the Nix store.
|
||||
writeSecrets = pkgs.writeShellScript "restic-write-secrets" ''
|
||||
set -euo pipefail
|
||||
mkdir -p ${runtimeDir}
|
||||
${pkgs.sops}/bin/sops \
|
||||
--decrypt --extract '["restic_password"]' \
|
||||
--input-type yaml --output-type yaml ${secretsFile} \
|
||||
| tr -d '\n' > "${passwordFile}"
|
||||
chmod 0600 "${passwordFile}"
|
||||
|
||||
# Prune backups older than 14 days
|
||||
find "$BACKUP_DIR" -name "weekly-backup-*" -mtime +14 -delete
|
||||
: > "${environmentFile}"
|
||||
chmod 0600 "${environmentFile}"
|
||||
${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} |
|
||||
${pkgs.gnused}/bin/sed -nE \
|
||||
's/^b2_key_id: (.*)/B2_ACCOUNT_ID=\1/p; s/^b2_application_key: (.*)/B2_ACCOUNT_KEY=\1/p' \
|
||||
>> "${environmentFile}"
|
||||
'';
|
||||
in {
|
||||
systemd.services.weekly-backup = {
|
||||
description = "Weekly backup of home directory";
|
||||
path = with pkgs; [coreutils gnutar findutils];
|
||||
systemd.services.restic-password = {
|
||||
description = "Materialize restic repository password and B2 credentials from sops";
|
||||
wantedBy = ["multi-user.target"];
|
||||
# The age key lives in /etc/age/keys.txt; the service must know where it is
|
||||
# and needs a HOME for age to report its user config directory.
|
||||
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${backupScript}";
|
||||
User = "root";
|
||||
Environment = ["HOME=/root"];
|
||||
ExecStart = "${writeSecrets}";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.weekly-backup = {
|
||||
wantedBy = ["timers.target"];
|
||||
# B2 credentials are supplied via environmentFile (B2_ACCOUNT_ID / B2_ACCOUNT_KEY),
|
||||
# matching the official module example which combines `repository` and `environmentFile`.
|
||||
services.restic.backups.localbackup = {
|
||||
repository = "b2:hetzner-severijnse";
|
||||
environmentFile = environmentFile;
|
||||
passwordFile = passwordFile;
|
||||
initialize = true;
|
||||
paths = [
|
||||
"/home/admin"
|
||||
"/var/lib/postgresql"
|
||||
"/var/lib/gitea"
|
||||
"/var/lib/caddy"
|
||||
"/var/lib/virtualcam"
|
||||
"/var/lib/stalwart"
|
||||
"/var/lib/bulwark"
|
||||
"/var/lib/coredns"
|
||||
"/etc/nixos"
|
||||
];
|
||||
exclude = [
|
||||
"/home/admin/backups"
|
||||
"/home/admin/.opencode"
|
||||
"/home/admin/.local"
|
||||
"/home/admin/.npm"
|
||||
"/home/admin/.config"
|
||||
"*.log"
|
||||
"*.log.*"
|
||||
"**/.cache"
|
||||
];
|
||||
timerConfig = {
|
||||
OnCalendar = "Mon *-*-* 03:00:00";
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = "15m";
|
||||
};
|
||||
pruneOpts = [
|
||||
"--keep-daily 7"
|
||||
"--keep-weekly 4"
|
||||
"--keep-monthly 6"
|
||||
];
|
||||
runCheck = true;
|
||||
};
|
||||
|
||||
# The backup must never run before the secrets exist.
|
||||
systemd.services."restic-backups-localbackup" = {
|
||||
requires = ["restic-password.service"];
|
||||
after = ["restic-password.service"];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{...}: let
|
||||
{unstablePkgs, ...}: 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).*"
|
||||
@@ -12,6 +12,7 @@ in {
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
group = "caddy";
|
||||
package = unstablePkgs.caddy;
|
||||
dataDir = "/var/lib/caddy";
|
||||
logDir = "/var/log/caddy";
|
||||
globalConfig = ''
|
||||
@@ -23,9 +24,16 @@ in {
|
||||
# Global Caddyfile snippets (shared across all virtual hosts).
|
||||
extraConfig = ''
|
||||
(admin_gate) {
|
||||
@notvpn not remote_ip 10.8.0.0/24
|
||||
@notvpn not remote_ip 127.0.0.1 ::1 10.8.0.0/24 fd10:8::/64 49.13.92.205 2a01:4f8:c014:2585::1
|
||||
respond @notvpn "Forbidden" 403
|
||||
}
|
||||
# Restrict access to the server itself (loopback + public IPs) or the
|
||||
# WireGuard VPN. Gitea reaches this from the server when publishing a
|
||||
# release, and VPN clients can download from it; the public cannot.
|
||||
(release_gate) {
|
||||
@deny not remote_ip 127.0.0.1 ::1 10.8.0.0/24 fd10:8::/64 49.13.92.205 2a01:4f8:c014:2585::1
|
||||
respond @deny "Forbidden" 403
|
||||
}
|
||||
# Security headers applied to every response of every site that
|
||||
# imports this snippet (covers all current and future hosts).
|
||||
# For proxied hosts whose upstream sets its own copies, strip them
|
||||
@@ -133,7 +141,6 @@ in {
|
||||
import security_headers
|
||||
import csp
|
||||
${antiScrape}
|
||||
import admin_gate
|
||||
header Strict-Transport-Security "max-age=31536000;"
|
||||
reverse_proxy 127.0.0.1:1001
|
||||
encode zstd gzip
|
||||
@@ -154,13 +161,33 @@ in {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
${antiScrape}
|
||||
reverse_proxy 127.0.0.1:8888 {
|
||||
# Strip copies set by the upstream SnappyMail container so we
|
||||
# emit exactly one correct value of each security header.
|
||||
header_down -X-Frame-Options
|
||||
header_down -X-XSS-Protection
|
||||
header_down -X-Content-Type-Options
|
||||
# Stalwart JMAP API (http-management listener on 8080): required so the
|
||||
# browser can reach /.well-known/jmap, /jmap/session and the JMAP
|
||||
# upload/download/eventsource/ws paths at the public origin.
|
||||
handle /.well-known/jmap {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
# NOTE: use handle (not handle_path) so the /jmap prefix is preserved.
|
||||
handle /jmap {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
handle /jmap/* {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
handle {
|
||||
# Bulwark webmail (JMAP client for Stalwart), running on host port 3002.
|
||||
reverse_proxy 127.0.0.1:3002
|
||||
}
|
||||
encode zstd gzip
|
||||
'';
|
||||
};
|
||||
|
||||
"admin.mail.severijnse.eu" = {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
${antiScrape}
|
||||
# Stalwart webadmin UI (served by the http-management listener on 8080).
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
encode zstd gzip
|
||||
'';
|
||||
};
|
||||
@@ -218,12 +245,48 @@ in {
|
||||
import security_headers
|
||||
import csp
|
||||
${antiScrape}
|
||||
import admin_gate
|
||||
reverse_proxy 127.0.0.1:5000
|
||||
encode zstd gzip
|
||||
'';
|
||||
};
|
||||
|
||||
"virtualcam.severijnse.eu" = {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
import csp
|
||||
${antiScrape}
|
||||
basic_auth {
|
||||
chan $2a$14$7ZeNpGT0L68uZwzdWBcF0OulzhrYbfAs232Ojt//LHQ1qvXU4x32O
|
||||
}
|
||||
reverse_proxy 127.0.0.1:3001
|
||||
encode zstd gzip
|
||||
'';
|
||||
};
|
||||
|
||||
"api.severijnse.eu" = {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
# Machine-facing license API for the Android app: no antiScrape
|
||||
# bot-blocking (it would drop legitimate non-browser clients) and no
|
||||
# admin_gate (endpoints are protected by the bearer token).
|
||||
reverse_proxy 127.0.0.1:3004
|
||||
encode zstd gzip
|
||||
'';
|
||||
};
|
||||
|
||||
"releases.severijnse.eu" = {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
root * /srv/releases
|
||||
file_server browse
|
||||
encode zstd gzip
|
||||
|
||||
handle /private* {
|
||||
import release_gate
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
"http://ip.severijnse.eu" = {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
@@ -238,5 +301,6 @@ in {
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /srv 0755 caddy caddy -"
|
||||
"d /srv/jory 0755 caddy caddy -"
|
||||
"d /srv/releases 0755 admin caddy -"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
zoneFile = pkgs.writeText "severijnse.eu.db" ''
|
||||
$ORIGIN severijnse.eu.
|
||||
$TTL 3600
|
||||
severijnse.eu. 3600 IN SOA ns1.severijnse.eu. abuse.severijnse.eu. 2026071003 3600 1800 1209600 86400
|
||||
severijnse.eu. 3600 IN SOA ns1.severijnse.eu. abuse.severijnse.eu. 2026071004 3600 1800 1209600 86400
|
||||
IN NS ns1.severijnse.eu.
|
||||
IN NS ns2.severijnse.eu.
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
ns1 IN A 49.13.92.205
|
||||
ns2 IN A 49.13.92.205
|
||||
mail IN A 49.13.92.205
|
||||
admin.mail IN A 49.13.92.205
|
||||
|
||||
@ IN AAAA 2a01:4f8:c014:2585::1
|
||||
www IN AAAA 2a01:4f8:c014:2585::1
|
||||
ns1 IN AAAA 2a01:4f8:c014:2585::1
|
||||
ns2 IN AAAA 2a01:4f8:c014:2585::1
|
||||
mail IN AAAA 2a01:4f8:c014:2585::1
|
||||
admin.mail IN AAAA 2a01:4f8:c014:2585::1
|
||||
|
||||
*.severijnse.eu. IN A 49.13.92.205
|
||||
*.severijnse.eu. IN AAAA 2a01:4f8:c014:2585::1
|
||||
@@ -25,7 +27,7 @@
|
||||
|
||||
mail._domainkey.severijnse.eu. 300 IN TXT (
|
||||
"v=DKIM1; k=rsa; "
|
||||
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskpG7m4kninxRE4JF5KzpnBhLbOlGJL2RO/iDfzdz6sHEvxe78q9c5UnZ0OQddOSuQo4q48dJkXR/XzqY7Ak109lhoAx+Kr1neYsi8/"
|
||||
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskpG7m4kninxRE4JF5KzpnBhLbOlGJL2RO/iDfzdz6sHEvxe78q9c5UnZ0OQddOSuQo4q48dJkXR/XzqY7Ak109lhoAx+Kr1neYsi8K/"
|
||||
"JaoTC8OURk365+/aBSmWXUCCBphCx43QWfC9h8GMQ6PUIqawkz5CcGPT7X7hPdwHQcd5Vn3CqmABptxdwshdkBjZs"
|
||||
"oi79BOo9ZrQSTY7iiLcOP7hVVC9Ad+ydlZ4MWGfy5BxgyTGrrtuSuLcM219oqdovIvr2EtXs8AMx5fyXplKE3R/"
|
||||
"YlwF2Jcy50Gmb5y/E9pOaFjVv8HXUmKsvuhA2b8K+rt0WVHNc3dvbgZUl8bGAQIDAQAB"
|
||||
@@ -91,7 +93,8 @@ in {
|
||||
'';
|
||||
};
|
||||
coredns = {
|
||||
after = ["decrypt-coredns-keys.service"];
|
||||
after = ["decrypt-coredns-keys.service" "network-online.target"];
|
||||
wants = ["network-online.target"];
|
||||
requires = ["decrypt-coredns-keys.service"];
|
||||
|
||||
# Copy zone file from Nix store to writable location on service start
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
image = "crazymax/fail2ban:latest";
|
||||
autoStart = true;
|
||||
volumes = [
|
||||
"/home/admin/dms/mail-logs:/var/log/mail:ro"
|
||||
"/home/admin/fail2ban/data:/data:Z"
|
||||
];
|
||||
environment = {
|
||||
@@ -15,8 +14,4 @@
|
||||
"--cap-add=NET_ADMIN"
|
||||
];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/log/mail 0755 root root -"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
#
|
||||
# Netdata
|
||||
#
|
||||
services.netdata = {
|
||||
enable = true;
|
||||
config = {
|
||||
global = {
|
||||
"memory mode" = "ram";
|
||||
"debug log" = "none";
|
||||
"access log" = "none";
|
||||
"error log" = "syslog";
|
||||
};
|
||||
web = {
|
||||
"bind to" = "127.0.0.1:19999";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.caddy.virtualHosts."netdata.severijnse.eu" = {
|
||||
extraConfig = ''
|
||||
import security_headers
|
||||
import csp
|
||||
reverse_proxy 127.0.0.1:19999
|
||||
encode zstd gzip
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{...}: {
|
||||
virtualisation.oci-containers.containers.snappymail = {
|
||||
image = "djmaze/snappymail:latest";
|
||||
autoStart = true;
|
||||
ports = ["127.0.0.1:8888:8888"];
|
||||
volumes = [
|
||||
"/home/admin/snappymail-data:/var/lib/snappymail:Z"
|
||||
];
|
||||
environment = {
|
||||
TZ = "Europe/Berlin";
|
||||
};
|
||||
extraOptions = [
|
||||
"--label=com.centurylinklabs.watchtower.enable=true"
|
||||
];
|
||||
};
|
||||
|
||||
# Ensure the persistent data dir exists so podman's :Z relabel (statfs) succeeds on first boot.
|
||||
# Owned by 82:82 (www-data) because the container's PHP worker runs as UID 82 and must be
|
||||
# able to write to /var/lib/snappymail (SnappyMail checks is_writable on that path).
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /home/admin/snappymail-data 0755 82 82 - -"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
unstablePkgs,
|
||||
...
|
||||
}: let
|
||||
# Caddy's dist dir (see tlsa-updater.nix): cert 0644, key 0640 root:root.
|
||||
# The Stalwart service runs as "stalwart"; grant it read access to the key.
|
||||
certDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
||||
# sops-encrypted secrets (single file holds all service secrets), same as backup.nix.
|
||||
secretsFile = ../../secrets/secrets.yaml;
|
||||
# Root-only runtime file holding the fallback-admin password hash (0600 root).
|
||||
adminHashFile = "/var/lib/stalwart/.admin-hash";
|
||||
|
||||
# Materialize the fallback-admin password hash from sops into a root-only file.
|
||||
writeAdminHash = pkgs.writeShellScript "stalwart-write-admin-hash" ''
|
||||
set -euo pipefail
|
||||
install -d -o root -g root -m 0755 "$(dirname ${adminHashFile})"
|
||||
${pkgs.sops}/bin/sops \
|
||||
--decrypt --extract '["stalwart_admin_hash"]' \
|
||||
--input-type yaml --output-type yaml ${secretsFile} \
|
||||
| tr -d '\n' > "${adminHashFile}"
|
||||
chmod 0600 "${adminHashFile}"
|
||||
'';
|
||||
|
||||
# The outbound DKIM key is the existing opendkim key from docker-mailserver
|
||||
# (selector "mail", domain severijnse.eu, PKCS#8 RSA 2048). Install it into the
|
||||
# stalwart-owned data dir so the server can read it and it is covered by the
|
||||
# /var/lib/stalwart backup.
|
||||
dkimKeyDir = "/var/lib/stalwart/dkim/severijnse.eu";
|
||||
dkimKeySrc = "/home/admin/dms/config/opendkim/keys/severijnse.eu/mail.private";
|
||||
writeDkimKey = pkgs.writeShellScript "stalwart-write-dkim-key" ''
|
||||
set -euo pipefail
|
||||
install -d -o stalwart -g stalwart -m 0750 ${dkimKeyDir}
|
||||
install -o stalwart -g stalwart -m 0640 ${dkimKeySrc} ${dkimKeyDir}/mail.private
|
||||
'';
|
||||
in {
|
||||
# The hetzner host is built with nixos-24.05, which ships its own
|
||||
# `services.stalwart-mail` module (for the old 0.8.x package). We want the
|
||||
# 0.15.5 module from the locked nixpkgs-unstable instead, so we must exclude
|
||||
# the 24.05 default module (which defines the same option namespace) to avoid
|
||||
# the rename-based infinite recursion, and import the unstable one in its place.
|
||||
disabledModules = [
|
||||
"services/mail/stalwart-mail.nix"
|
||||
];
|
||||
|
||||
imports = [
|
||||
"${unstablePkgs.path}/nixos/modules/services/mail/stalwart.nix"
|
||||
];
|
||||
|
||||
services.stalwart = {
|
||||
enable = true;
|
||||
stateVersion = "26.05";
|
||||
package = unstablePkgs.stalwart;
|
||||
|
||||
# Public listeners. The host firewall (networking.nix) already allows
|
||||
# 25/143/465/587/993, so openFirewall stays off: the module would also open
|
||||
# every other listener port incl. the 8080 webadmin.
|
||||
openFirewall = false;
|
||||
|
||||
settings = {
|
||||
# EHLO / hostname for the server (docs server.hostname).
|
||||
server.hostname = "mail.severijnse.eu";
|
||||
|
||||
# Public origin the JMAP/webadmin API is served from, advertised in the
|
||||
# JMAP session (docs server/core/network.md "http.url"). Value must be a
|
||||
# JScript expression, hence the single-quoted string literal. Without this
|
||||
# Stalwart advertises http://mail.severijnse.eu:8080, which the browser
|
||||
# refuses to fetch and breaks Bulwark webmail.
|
||||
http.url = "'https://mail.severijnse.eu'";
|
||||
|
||||
certificate."mail-severijnse-eu" = {
|
||||
cert = "%{file:${certDir}/mail.severijnse.eu.crt}%";
|
||||
private-key = "%{file:${certDir}/mail.severijnse.eu.key}%";
|
||||
# Docs server/tls/certificates.md: used when the client sends no SNI.
|
||||
default = true;
|
||||
};
|
||||
|
||||
server.tls = {
|
||||
certificate = "mail-severijnse-eu";
|
||||
enable = true;
|
||||
implicit = false;
|
||||
};
|
||||
|
||||
# Public listeners (docs server/listener.md + protocol, tls.implicit override).
|
||||
# Bind "[::]:port" for dual-stack IPv4+IPv6 (docs: "to bind a listener to
|
||||
# all interfaces"); listing both 0.0.0.0 and [::] makes the [::] bind fail
|
||||
# with EADDRINUSE on kernels with net.ipv6.bindv6only=0.
|
||||
# 143/587 use STARTTLS (server.tls.implicit=false default), 993/465 the
|
||||
# implicit-TLS variants, 25 the plain (STARTTLS) MX port.
|
||||
server.listener = {
|
||||
"imap" = {
|
||||
bind = ["[::]:143"];
|
||||
protocol = "imap";
|
||||
};
|
||||
"imaps" = {
|
||||
bind = ["[::]:993"];
|
||||
protocol = "imap";
|
||||
tls.implicit = true;
|
||||
};
|
||||
"smtp" = {
|
||||
bind = ["[::]:25"];
|
||||
protocol = "smtp";
|
||||
};
|
||||
"smtp-submission" = {
|
||||
bind = ["[::]:587"];
|
||||
protocol = "smtp";
|
||||
};
|
||||
"smtp-submissions" = {
|
||||
bind = ["[::]:465"];
|
||||
protocol = "smtp";
|
||||
tls.implicit = true;
|
||||
};
|
||||
"http-management" = {
|
||||
bind = ["127.0.0.1:8080"];
|
||||
protocol = "http";
|
||||
};
|
||||
};
|
||||
|
||||
# Auth per docs mta/inbound/auth.md (AUTH stage): authentication is
|
||||
# disabled on the plain SMTP listener (port 25) and required everywhere
|
||||
# else (IMAP + submission). Only offer PLAIN/LOGIN over TLS, so clear-text
|
||||
# listeners (143/587 pre-STARTTLS) advertise no SASL mechanisms. This
|
||||
# mirrors the code defaults (crates/common/src/config/smtp/session.rs).
|
||||
# The JMAP/webadmin "http-management" listener (bind 127.0.0.1:8080) is
|
||||
# plain HTTP and reachable only from localhost, where bulwark connects;
|
||||
# its basic-auth uses the "plain" mechanism, so it must be exempted from
|
||||
# the is_tls gate or webmail logins fail with "Authentication not allowed".
|
||||
session.auth.mechanisms = [
|
||||
{
|
||||
"if" = "local_port != 25 && (is_tls || listener == 'http-management')";
|
||||
"then" = "[plain, login]";
|
||||
}
|
||||
{"else" = false;}
|
||||
];
|
||||
session.auth.directory = [
|
||||
{
|
||||
"if" = "listener != 'smtp'";
|
||||
"then" = "'internal'";
|
||||
}
|
||||
{"else" = false;}
|
||||
];
|
||||
session.auth.require = [
|
||||
{
|
||||
"if" = "listener != 'smtp'";
|
||||
"then" = true;
|
||||
}
|
||||
{"else" = false;}
|
||||
];
|
||||
|
||||
# Outbound DKIM signing (docs mta/authentication/dkim/sign): sign with the
|
||||
# "mail" signature on everything submitted via non-25 listeners; do not sign
|
||||
# inbound mail received on the plain "smtp" listener.
|
||||
auth.dkim.sign = [
|
||||
{
|
||||
"if" = "listener != 'smtp'";
|
||||
"then" = "['mail']";
|
||||
}
|
||||
{"else" = false;}
|
||||
];
|
||||
|
||||
# ARC sealing uses the same "mail" signature (docs mta/authentication/arc).
|
||||
# The code default ('rsa-' + report.domain) would reference a signature
|
||||
# name that does not exist and log "ARC sealer not found".
|
||||
auth.arc.seal = "'mail'";
|
||||
|
||||
# Every *downstream* sign rule defaults to signing with
|
||||
# ['rsa-<report.domain>', 'ed25519-<report.domain>'] (queue.rs/report.rs),
|
||||
# names that do not exist here and log "DKIM signer not found" on DSNs and
|
||||
# reports. Point them all at the real "mail" signature.
|
||||
report.dsn.sign = "['mail']";
|
||||
report.spf.sign = "['mail']";
|
||||
report.dmarc.sign = "['mail']";
|
||||
report.dmarc.aggregate.sign = "['mail']";
|
||||
report.tls.aggregate.sign = "['mail']";
|
||||
|
||||
# Reuse the existing opendkim key (selector mail) so no DNS change is needed.
|
||||
signature.mail = {
|
||||
private-key = "%{file:/var/lib/stalwart/dkim/severijnse.eu/mail.private}%";
|
||||
domain = "severijnse.eu";
|
||||
selector = "mail";
|
||||
headers = ["From" "To" "Date" "Subject" "Message-Id"];
|
||||
algorithm = "rsa-sha256";
|
||||
canonicalization = "relaxed/relaxed";
|
||||
set-body-length = false;
|
||||
};
|
||||
|
||||
# Fallback admin (auth/authorization/administrator.md): bootstrap admin with
|
||||
# every permission, used to create the internal-directory accounts via the
|
||||
# management REST API / CLI. Secret is a SHA-512-crypt hash, injected via
|
||||
# LoadCredential (services.stalwart.credentials) so no secret lands in the
|
||||
# Nix store.
|
||||
authentication."fallback-admin" = {
|
||||
user = "admin";
|
||||
secret = "%{file:/run/credentials/stalwart.service/stalwart-admin}%";
|
||||
};
|
||||
|
||||
# Route docs mta/outbound/routing.md + strategy.md: local domains → local
|
||||
# store, everything else → MX. The strategy names are defined explicitly
|
||||
# (docs define queue.route.mx/local; the built-in fallback in
|
||||
# core.rs:get_route_or_default only kicks in for undeclared names).
|
||||
queue.strategy.route = [
|
||||
{
|
||||
"if" = "is_local_domain('', rcpt_domain)";
|
||||
"then" = "'local'";
|
||||
}
|
||||
{"else" = "'mx'";}
|
||||
];
|
||||
queue.route."mx" = {
|
||||
type = "mx";
|
||||
ip-lookup = "ipv4_then_ipv6";
|
||||
};
|
||||
queue.route."local" = {
|
||||
type = "local";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# The module's service runs as user/group "stalwart" (ProtectHome=true,
|
||||
# ProtectSystem=strict). The TLS key tlsa-updater installs is 0640 root:root;
|
||||
# regrant it to the stalwart group after every cert sync so stalwart can serve TLS.
|
||||
systemd.services.stalwart = {
|
||||
after = ["tlsa-update.service" "stalwart-admin-secret.service" "stalwart-dkim.service"];
|
||||
requires = ["tlsa-update.service" "stalwart-admin-secret.service" "stalwart-dkim.service"];
|
||||
};
|
||||
|
||||
# Make the management CLI available for account creation and maildir import
|
||||
# (docs management/cli/). Version-pinned to the locked unstable nixpkgs.
|
||||
environment.systemPackages = [unstablePkgs.stalwart-cli];
|
||||
|
||||
# Materialize the fallback-admin password hash from sops before stalwart starts.
|
||||
# The admin hash is a SHA-512-crypt value, safe to pass through a root-only file.
|
||||
systemd.services.stalwart-admin-secret = {
|
||||
description = "Materialize Stalwart fallback-admin password hash from sops";
|
||||
wantedBy = ["multi-user.target"];
|
||||
before = ["stalwart.service"];
|
||||
# The age key lives in /etc/age/keys.txt; the service must know where it is
|
||||
# and needs a HOME for age to report its user config directory.
|
||||
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
Environment = ["HOME=/root"];
|
||||
ExecStart = "${writeAdminHash}";
|
||||
};
|
||||
};
|
||||
|
||||
# Install the outbound DKIM key into the stalwart data dir before the service starts.
|
||||
systemd.services.stalwart-dkim = {
|
||||
description = "Install Stalwart outbound DKIM key";
|
||||
wantedBy = ["multi-user.target"];
|
||||
before = ["stalwart.service"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${writeDkimKey}";
|
||||
};
|
||||
};
|
||||
|
||||
# LoadCredential: expose the materialized hash to stalwart only at
|
||||
# /run/credentials/stalwart.service/stalwart-admin (see `credentials` option
|
||||
# in the upstream module; the value is the source path on disk).
|
||||
services.stalwart.credentials.stalwart-admin = adminHashFile;
|
||||
|
||||
systemd.services.stalwart-cert-perm = {
|
||||
description = "Grant stalwart read access to its TLS private key";
|
||||
# Belt-and-suspenders: tlsa-update already chgrps the key after every sync;
|
||||
# this guarantees the group grant also exists at first boot, before stalwart
|
||||
# starts (previously ordered after stalwart, so a fresh sync could leave a
|
||||
# root:root key and webadmin reload would fail with EACCES).
|
||||
after = ["tlsa-update.service"];
|
||||
before = ["stalwart.service"];
|
||||
partOf = ["tlsa-update.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = [pkgs.coreutils];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.coreutils}/bin/chgrp stalwart ${certDir}/mail.severijnse.eu.key";
|
||||
ExecStartPost = "${pkgs.coreutils}/bin/chmod 0640 ${certDir}/mail.severijnse.eu.key";
|
||||
};
|
||||
};
|
||||
|
||||
# Bulwark webmail (self-hosted JMAP webmail for Stalwart). Serving on
|
||||
# mail.severijnse.eu behind Caddy (see caddy.nix). It connects to Stalwart's
|
||||
# JMAP endpoint at 127.0.0.1:8080, so uses host networking. Next.js defaults
|
||||
# to POST_SIZE/etc via env; JMAP_SERVER_URL points at the Stalwart http
|
||||
# listener which serves JMAP at /jmap.
|
||||
virtualisation.oci-containers.containers.bulwark = {
|
||||
image = "ghcr.io/bulwarkmail/webmail:latest";
|
||||
autoStart = true;
|
||||
volumes = [
|
||||
"/var/lib/bulwark:/app/data:Z"
|
||||
];
|
||||
environment = {
|
||||
# JMAP_SERVER_URL is the public origin (the browser uses it verbatim for
|
||||
# /.well-known/jmap + the session apiUrl). HOSTNAME stays 127.0.0.1 so
|
||||
# Next.js binds to loopback (caddy reverse_proxy's 127.0.0.1:3002); binding
|
||||
# to the public hostname made the container unreachable for caddy (502).
|
||||
JMAP_SERVER_URL = "https://mail.severijnse.eu";
|
||||
HOSTNAME = "127.0.0.1";
|
||||
PORT = "3002";
|
||||
};
|
||||
extraOptions = [
|
||||
"--network=host"
|
||||
"--label=com.centurylinklabs.watchtower.enable=true"
|
||||
];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/bulwark 0755 1001 1001 - -"
|
||||
];
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
{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.
|
||||
# owned caddy:caddy 0600 — stalwart cannot read it directly, so we copy it
|
||||
# into a distribution dir stalwart's service (user "stalwart") can reach.
|
||||
caddyCertDir = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
||||
# World-readable distribution dir mounted (RO) into the mail server container.
|
||||
# Distribution dir read by stalwart (cert 0644, private key regranted to the
|
||||
# "stalwart" group by systemd.services.stalwart-cert-perm).
|
||||
distCertDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
||||
zoneFile = "/var/lib/coredns/zones/severijnse.eu.db";
|
||||
# Records the SPKI hash applied at the last restart of stalwart, so cert
|
||||
# renewals trigger exactly one restart and unchanged certs never do.
|
||||
stateFile = "/var/lib/tlsa-update/.last-spki";
|
||||
|
||||
syncScript = pkgs.writeShellScript "tlsa-update" ''
|
||||
set -euo pipefail
|
||||
@@ -22,13 +26,17 @@
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1) Propagate Caddy's renewed certificate into the distribution dir the
|
||||
# mail server mounts. Caddy stores certs 0600 caddy:caddy. The cert is
|
||||
# world-readable (Postfix/Dovecot read it as root before dropping
|
||||
# privileges); the private key is restricted to root (0640) so it is not
|
||||
# exposed to other local users.
|
||||
# 1) Propagate Caddy's renewed certificate into the distribution dir stalwart
|
||||
# reads. Caddy stores certs 0600 caddy:caddy. The cert is world-readable;
|
||||
# the private key is restricted to root (0640) and stalwart-cert-perm
|
||||
# regrants it to the "stalwart" group so it is not exposed to other local
|
||||
# users.
|
||||
install -D -m 0644 "$SRC_CERT" "$DST_CERT"
|
||||
install -D -m 0640 "$SRC_KEY" "$DST_KEY"
|
||||
# Stalwart reads the key as user "stalwart" via %{file:...}%; regrant the
|
||||
# group immediately so every sync leaves it readable (0640 root:stalwart)
|
||||
# and webadmin config reload never fails with EACCES.
|
||||
chgrp stalwart "$DST_KEY"
|
||||
|
||||
# 2) TLSA 3 1 1 = SHA-256 of the certificate's SubjectPublicKeyInfo (SPKI),
|
||||
# NOT the whole certificate. Matching type 1 = SHA-256 of the SPKI DER.
|
||||
@@ -47,8 +55,24 @@
|
||||
|
||||
# 4) Reload services so the changes take effect immediately.
|
||||
systemctl reload coredns.service || true
|
||||
podman exec mailserver postfix reload || true
|
||||
podman exec mailserver dovecot reload || true
|
||||
# Stalwart reads its TLS certs ($certDir) via %{file:...}% placeholders at
|
||||
# startup only; there is no signal-based reload (management API reload needs
|
||||
# admin credentials we must not store). Restart it, but only when the cert
|
||||
# actually changed (SPKI hash differs from the last applied run), so the
|
||||
# daily timer and inotify events for unchanged certs do not drop connections.
|
||||
# NOTE: stalwart.service `requires` this unit, so the restart must be issued
|
||||
# with --no-block (async): a synchronous restart waits for stalwart to come
|
||||
# back up, which in turn waits for this unit to finish — a deadlock. The state
|
||||
# file is updated BEFORE the restart so the tlsa-update run that stalwart's
|
||||
# required-activation re-triggers sees a matching hash and exits immediately.
|
||||
if [ "$(cat "${stateFile}" 2>/dev/null || true)" != "$HEX" ]; then
|
||||
printf '%s\n' "$HEX" > "${stateFile}"
|
||||
# Restarting stalwart re-runs its Requires=tlsa-update dependency; guard
|
||||
# with is-active so a boot-time run never races stalwart's initial start.
|
||||
if systemctl is-active --quiet stalwart.service 2>/dev/null; then
|
||||
systemctl --no-block restart stalwart.service || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "tlsa-update: TLSA set to $HEX"
|
||||
'';
|
||||
@@ -57,6 +81,7 @@ in {
|
||||
# Ensure the distribution dir exists (Caddy does not write here).
|
||||
tmpfiles.rules = [
|
||||
"d ${distCertDir} 0755 root root - -"
|
||||
"d /var/lib/tlsa-update 0755 root root - -"
|
||||
];
|
||||
|
||||
services.tlsa-update = {
|
||||
@@ -64,7 +89,7 @@ in {
|
||||
after = ["caddy.service" "coredns.service"];
|
||||
partOf = ["coredns.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = with pkgs; [openssl coreutils gnused podman systemd];
|
||||
path = with pkgs; [openssl coreutils gnused systemd];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${syncScript}";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
--- a/src/app/api/checkout/route.ts
|
||||
+++ b/src/app/api/checkout/route.ts
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
+const BYPASS_PAYMENTS = process.env.BYPASS_PAYMENTS === "true";
|
||||
+
|
||||
export async function POST(req: Request) {
|
||||
// Each checkout hits the payment provider, so cap order creation per user
|
||||
// and per IP to prevent order spam and provider-API abuse.
|
||||
@@ -86,6 +88,20 @@
|
||||
const paid = await prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { license: true },
|
||||
+ });
|
||||
+ return NextResponse.json({
|
||||
+ ok: true,
|
||||
+ orderId: order.id,
|
||||
+ licenseKey: paid?.license?.key ?? null,
|
||||
+ tier: tier.slug,
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ if (BYPASS_PAYMENTS) {
|
||||
+ await markOrderPaid({ orderId: order.id, providerRef: "bypass-test" });
|
||||
+ const paid = await prisma.order.findUnique({
|
||||
+ where: { id: order.id },
|
||||
+ include: { license: true },
|
||||
});
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
@@ -0,0 +1,287 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
unstablePkgs,
|
||||
...
|
||||
}: let
|
||||
rev = "6e351ddc732421eb775eb9843ccd2389d6525edd";
|
||||
apiRev = "390878d126185b21a14479f85c70134a155c61c6";
|
||||
|
||||
# Private repositories are fetched over SSH (port 2222). nix-daemon runs as
|
||||
# root and uses /root/.ssh (identity materialized by the git-ssh-key unit),
|
||||
# so the source ends up in the store without any Nix-native credentials.
|
||||
src = builtins.fetchGit {
|
||||
url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-website.git";
|
||||
rev = rev;
|
||||
};
|
||||
|
||||
srcPatched = pkgs.applyPatches {
|
||||
name = "virtualcam-website-patched";
|
||||
src = src;
|
||||
patches = [./virtualcam-checkout.patch];
|
||||
};
|
||||
|
||||
apiSrc = builtins.fetchGit {
|
||||
url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-api.git";
|
||||
rev = apiRev;
|
||||
};
|
||||
|
||||
# Build the Next.js app entirely in Nix (offline npm deps from the lockfile).
|
||||
app = unstablePkgs.buildNpmPackage {
|
||||
pname = "virtualcam-website";
|
||||
version = "0.1.0";
|
||||
src = srcPatched;
|
||||
npmDepsHash = "sha256-GodWQKtOtsLOjjiwzxun+wTPhtvjR2uCV91n+wiZHw4=";
|
||||
nodejs = unstablePkgs.nodejs;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
npx prisma generate
|
||||
npm run build
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
cp -r .next node_modules public prisma src package.json package-lock.json \
|
||||
prisma.config.ts next.config.ts tsconfig.json postcss.config.mjs $out/
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
APP_URL = "https://virtualcam.severijnse.eu";
|
||||
GOOGLE_CLIENT_ID = "754775011707-c699m092tv3icmovhk5qa106v3q6eh7c.apps.googleusercontent.com";
|
||||
# Only used to satisfy prisma generate / next build metadata resolution.
|
||||
DATABASE_URL = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
||||
# Use the nixpkgs-bundled Prisma engine so the sandboxed offline build does
|
||||
# not need to download it from binaries.prisma.sh. Version matches ^7.8.0.
|
||||
PRISMA_SCHEMA_ENGINE_BINARY = "${unstablePkgs.prisma-engines}/bin/schema-engine";
|
||||
NEXT_TELEMETRY_DISABLED = "1";
|
||||
CI = "true";
|
||||
};
|
||||
|
||||
# License validation / admin API (Go/Fiber), serving on host port 3004.
|
||||
# go.mod demands go 1.26.5 but the pinned nixpkgs only has 1.26.4; the code
|
||||
# uses nothing newer, so the directive is relaxed to match the toolchain.
|
||||
api = unstablePkgs.buildGoModule {
|
||||
pname = "virtualcam-api";
|
||||
version = "0.1.0";
|
||||
src = apiSrc;
|
||||
vendorHash = "sha256-uvHClXHw9ycoIf6qBZmV2O3CSyIxCgnSPCSALM07qg8=";
|
||||
go = unstablePkgs.go_1_26;
|
||||
postPatch = ''
|
||||
sed -i 's/^go 1\.26\.5$/go 1.26.4/' go.mod
|
||||
'';
|
||||
};
|
||||
|
||||
dbUrl = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
||||
# lib/pq parses the DSN differently from node-postgres: a hostname in the URL
|
||||
# authority wins over a `host=` query param (so it would go over TCP and fail
|
||||
# password auth), and lib/pq rejects unknown URL params like `schema`. Use a
|
||||
# keyword DSN: unix-socket + peer auth + no SSL, matching the OS user.
|
||||
apiDbUrl = "host=/run/postgresql user=virtualcam dbname=virtualcam sslmode=disable";
|
||||
|
||||
# Root-only runtime environment file holding the secrets both services need.
|
||||
secretsFile = ../../secrets/secrets.yaml;
|
||||
envFile = "/var/lib/virtualcam/environment";
|
||||
|
||||
# Materialize the license signing key and admin token from sops into a
|
||||
# root-only file (0600). systemd reads environmentFiles before dropping
|
||||
# privileges, so the service users never need to read it themselves.
|
||||
writeSecrets = pkgs.writeShellScript "virtualcam-write-secrets" ''
|
||||
set -euo pipefail
|
||||
install -d -o virtualcam -g virtualcam -m 0750 /var/lib/virtualcam
|
||||
: > "${envFile}"
|
||||
chmod 0600 "${envFile}"
|
||||
${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} |
|
||||
${pkgs.gnused}/bin/sed -nE \
|
||||
's/^virtualcam_license_signing_key: (.*)/LICENSE_SIGNING_KEY=\1/p; s/^virtualcam_admin_token: (.*)/ADMIN_TOKEN=\1/p; s/^virtualcam_smtp_pass: (.*)/SMTP_PASS=\1/p; s/^virtualcam_google_OAuth_secret: (.*)/GOOGLE_CLIENT_SECRET=\1/p; s/^shkeeper_api_key: (.*)/SHKEEPER_API_KEY=\1/p' \
|
||||
>> "${envFile}"
|
||||
'';
|
||||
|
||||
# Materialize the nix-daemon's SSH identity so private-repo fetches keep
|
||||
# working after the one-off bootstrap copy in /root/.ssh.
|
||||
writeGitSshKey = pkgs.writeShellScript "git-ssh-key" ''
|
||||
set -euo pipefail
|
||||
install -d -m 0700 /root/.ssh
|
||||
${pkgs.sops}/bin/sops --decrypt --extract '["git_ssh_key_b64"]' \
|
||||
--input-type yaml --output-type yaml ${secretsFile} |
|
||||
${pkgs.coreutils}/bin/base64 -d > /root/.ssh/id_ed25519
|
||||
chmod 0600 /root/.ssh/id_ed25519
|
||||
cat > /root/.ssh/known_hosts <<'EOF'
|
||||
git.severijnse.eu ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzIVo9Jdp8kwnWmTn26Fj68baJjwDphYw/0HTH5BzYY
|
||||
[git.severijnse.eu]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGcPKlvdxApYRCZuQxrljEdETrxAu0JsEJ+28zFaMfA3P69J9vEkqqAZZ0Uq3rtE8ZqPBxeDh7JYn2XChb+QVGjOHVWMCWqcdD43ws26eY91aHGYnO2kW+sKSxIh803tdifIp2q8VRnaHVXcLHLx2rmwmvqIfMLnEEShxAt4Q846DianBoA7S8heoBvMLi8AAO09sVEgMPWm8ULkoRq0poHslsKkdYKlWzecP85tPo/tlCEjQq5cC5+xidH9TS4ILxVIXGUfErZzbWZq5LtaRmqiU5U1+zzdb83oJvHFtTZ8wHwZX+VE4jZzKw7wdFPk+dJZp/3ZAVva6fLSjlpyyDFNWMcf2/18Gs6YioD1n2fr1dXruZiXNMnI19Dzyp+8yWIPbAeWyVx3BoCrcjIApEDMLMo7ut0l6RfNy80hYpY91VBrpsjSRLB4uCVDsqhTLPTrFUlu+9YY9lSHEUVLRylOLK54TbY089hbHOB/UdpyCiXpI0ds3ww1UmnDQSFjwNknc+wTXRvzEz+VsX2nAzfO+5NWq2jHNn4wH6GuAinAxSVg1A7Ub9VSmPelRiTzK6ZnplPm762MxcPFmo+y/5cszumUwypcyTS8MJIC57S/kabfsnjdt97K+9eT9X2LDZ2uIQgDS8qEgvbnvfXVmMoqakgBLYG7Embqc8BDzNsQ==
|
||||
EOF
|
||||
chmod 0600 /root/.ssh/known_hosts
|
||||
test -f /root/.ssh/id_ed25519.pub || ${pkgs.openssh}/bin/ssh-keygen -y -f /root/.ssh/id_ed25519 > /root/.ssh/id_ed25519.pub
|
||||
'';
|
||||
|
||||
# The repo's seed uses tsx (a devDependency buildNpmPackage drops) plus the
|
||||
# "@/..." path alias. nixpkgs ships tsx, which honours tsconfig paths, so we
|
||||
# add it to the service path rather than working around the missing dep.
|
||||
seed = pkgs.writeShellScript "virtualcam-seed" ''
|
||||
set -euo pipefail
|
||||
export DATABASE_URL="${dbUrl}"
|
||||
export PRISMA_SCHEMA_ENGINE_BINARY="${unstablePkgs.prisma-engines}/bin/schema-engine"
|
||||
export HOME=/var/lib/virtualcam
|
||||
cd ${app}
|
||||
tsx prisma/seed.ts
|
||||
'';
|
||||
|
||||
# One shared PostgreSQL server (existing system postgres). Each service gets
|
||||
# its own database + role. virtualcam authenticates over the Unix socket via
|
||||
# peer auth: the systemd services run as OS user `virtualcam`, which matches
|
||||
# the database role `virtualcam`, so no password is stored anywhere.
|
||||
migrate = pkgs.writeShellScript "virtualcam-migrate" ''
|
||||
set -euo pipefail
|
||||
export DATABASE_URL="${dbUrl}"
|
||||
# Use the local Prisma engine; no network download needed at runtime.
|
||||
export PRISMA_SCHEMA_ENGINE_BINARY="${unstablePkgs.prisma-engines}/bin/schema-engine"
|
||||
cd ${app}
|
||||
./node_modules/.bin/prisma migrate deploy
|
||||
'';
|
||||
in {
|
||||
users = {
|
||||
users.virtualcam = {
|
||||
isSystemUser = true;
|
||||
group = "virtualcam";
|
||||
description = "virtualcamera services user";
|
||||
};
|
||||
groups.virtualcam = {};
|
||||
};
|
||||
|
||||
services.postgresql = {
|
||||
ensureDatabases = ["virtualcam"];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "virtualcam";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
systemd = {
|
||||
services = {
|
||||
git-ssh-key = {
|
||||
description = "Materialize nix-daemon git SSH key from sops";
|
||||
wantedBy = ["multi-user.target"];
|
||||
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
Environment = ["HOME=/root"];
|
||||
ExecStart = "${writeGitSshKey}";
|
||||
};
|
||||
};
|
||||
|
||||
virtualcam-secrets = {
|
||||
description = "Materialize virtualcam secrets from sops";
|
||||
wantedBy = ["multi-user.target"];
|
||||
# The age key lives in /etc/age/keys.txt; the service must know where it
|
||||
# is and needs a HOME for age to report its user config directory.
|
||||
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
Environment = ["HOME=/root"];
|
||||
ExecStart = "${writeSecrets}";
|
||||
};
|
||||
};
|
||||
|
||||
virtualcam-migrate = {
|
||||
description = "Virtualcam Prisma migrations";
|
||||
after = ["postgresql.service"];
|
||||
requires = ["postgresql.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "virtualcam";
|
||||
Group = "virtualcam";
|
||||
StateDirectory = "virtualcam";
|
||||
StateDirectoryMode = "0750";
|
||||
ExecStart = "${migrate}";
|
||||
};
|
||||
};
|
||||
|
||||
virtualcam-seed = {
|
||||
description = "Virtualcam catalog seed";
|
||||
after = ["virtualcam-migrate.service"];
|
||||
requires = ["virtualcam-migrate.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = [unstablePkgs.nodejs unstablePkgs.tsx];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "virtualcam";
|
||||
Group = "virtualcam";
|
||||
ExecCondition = "!/var/lib/virtualcam/.seeded";
|
||||
ExecStart = "${seed}";
|
||||
ExecStartPost = "${pkgs.coreutils}/bin/touch /var/lib/virtualcam/.seeded";
|
||||
StateDirectory = "virtualcam";
|
||||
StateDirectoryMode = "0750";
|
||||
};
|
||||
};
|
||||
|
||||
virtualcam = {
|
||||
description = "Virtualcamera website (Next.js)";
|
||||
after = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
|
||||
requires = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = [unstablePkgs.nodejs];
|
||||
serviceConfig = {
|
||||
User = "virtualcam";
|
||||
Group = "virtualcam";
|
||||
WorkingDirectory = "${app}";
|
||||
EnvironmentFile = [envFile];
|
||||
ExecStart = "${app}/node_modules/.bin/next start -p 3001 -H 127.0.0.1";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
StateDirectory = "virtualcam";
|
||||
StateDirectoryMode = "0750";
|
||||
Environment = [
|
||||
"DATABASE_URL=${dbUrl}"
|
||||
"APP_URL=https://virtualcam.severijnse.eu"
|
||||
"GOOGLE_CLIENT_ID=754775011707-c699m092tv3icmovhk5qa106v3q6eh7c.apps.googleusercontent.com"
|
||||
"PAYMENTS_MODE=shkeeper"
|
||||
"BYPASS_PAYMENTS=false"
|
||||
"SHKEEPER_URL=https://pay.severijnse.eu"
|
||||
"SHKEEPER_CRYPTO=BTC"
|
||||
"SHKEEPER_FIAT=USD"
|
||||
"ADMIN_EMAILS=jory@severijnse.eu"
|
||||
"SMTP_HOST=mail.severijnse.eu"
|
||||
"SMTP_PORT=587"
|
||||
"SMTP_USER=no-reply@severijnse.eu"
|
||||
"SMTP_FROM=no-reply@severijnse.eu"
|
||||
"NODE_ENV=production"
|
||||
"NEXT_TELEMETRY_DISABLED=1"
|
||||
"HOME=/var/lib/virtualcam"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
virtualcam-api = {
|
||||
description = "Virtualcamera license API (Go/Fiber)";
|
||||
after = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
|
||||
requires = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
serviceConfig = {
|
||||
User = "virtualcam";
|
||||
Group = "virtualcam";
|
||||
WorkingDirectory = "${api}";
|
||||
EnvironmentFile = [envFile];
|
||||
ExecStart = "${api}/bin/virtualcam-api";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
StateDirectory = "virtualcam";
|
||||
StateDirectoryMode = "0750";
|
||||
Environment = [
|
||||
"PORT=3004"
|
||||
"DATABASE_URL=${apiDbUrl}"
|
||||
"CORS_ORIGINS=https://virtualcam.severijnse.eu"
|
||||
"HOME=/var/lib/virtualcam"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Served behind Caddy (virtualHosts wired in caddy.nix).
|
||||
networking.firewall.allowedTCPPorts = [];
|
||||
}
|
||||
@@ -1,16 +1,40 @@
|
||||
{...}: {
|
||||
{pkgs, ...}: let
|
||||
# sops-encrypted secrets (single file holds all service secrets), same as backup.nix.
|
||||
secretsFile = ../../secrets/secrets.yaml;
|
||||
# Root-only env file wg-easy reads the admin password from (0600 root).
|
||||
wgEnvFile = "/var/lib/wg-easy/environment";
|
||||
|
||||
# Materialize the wg-easy admin password from sops into a root-only env file,
|
||||
# so the secret never lands in the Nix store.
|
||||
writeSecrets = pkgs.writeShellScript "wg-easy-write-secrets" ''
|
||||
set -euo pipefail
|
||||
mkdir -p "$(dirname ${wgEnvFile})"
|
||||
PASSWORD="$(${pkgs.sops}/bin/sops \
|
||||
--decrypt --extract '["wg_admin_password"]' \
|
||||
--input-type yaml --output-type yaml ${secretsFile} | tr -d '\n')"
|
||||
printf 'INIT_PASSWORD=%s\n' "$PASSWORD" > "${wgEnvFile}"
|
||||
chmod 0600 "${wgEnvFile}"
|
||||
'';
|
||||
in {
|
||||
boot.kernelModules = ["ip6table_nat"];
|
||||
|
||||
virtualisation.oci-containers.containers.wg-easy = {
|
||||
image = "ghcr.io/wg-easy/wg-easy:latest";
|
||||
image = "ghcr.io/wg-easy/wg-easy:15";
|
||||
autoStart = true;
|
||||
volumes = [
|
||||
"/home/admin/config:/etc/wireguard:Z"
|
||||
];
|
||||
environmentFiles = [wgEnvFile];
|
||||
environment = {
|
||||
WG_HOST = "severijnse.eu";
|
||||
PASSWORD_HASH = "$2a$12$b3n4drXgS3B6ubMZxxjPUOQ1XktZ1EuDwm4AIdVulhtoD7b1.WQGC";
|
||||
WG_DEFAULT_ADDRESS = "10.8.0.x";
|
||||
WG_DEFAULT_DNS = "1.1.1.1";
|
||||
WG_PORT = "51820";
|
||||
INSECURE = "true";
|
||||
INIT_ENABLED = "true";
|
||||
INIT_USERNAME = "admin";
|
||||
INIT_HOST = "severijnse.eu";
|
||||
INIT_PORT = "51820";
|
||||
INIT_DNS = "1.1.1.1,2606:4700:4700::1111";
|
||||
INIT_IPV4_CIDR = "10.8.0.0/24";
|
||||
INIT_IPV6_CIDR = "fd10:8::/64";
|
||||
INIT_ALLOWED_IPS = "0.0.0.0/0, ::/0";
|
||||
};
|
||||
extraOptions = [
|
||||
"--cap-add=NET_ADMIN"
|
||||
@@ -19,4 +43,38 @@
|
||||
"--network=host"
|
||||
];
|
||||
};
|
||||
|
||||
# Materialize the wg-easy admin password from sops before the container starts.
|
||||
systemd.services.wg-easy-secrets = {
|
||||
description = "Materialize wg-easy admin password from sops";
|
||||
wantedBy = ["multi-user.target"];
|
||||
# The age key lives in /etc/age/keys.txt; the service must know where it is
|
||||
# and needs a HOME for age to report its user config directory.
|
||||
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
Environment = ["HOME=/root"];
|
||||
ExecStart = "${writeSecrets}";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services."podman-wg-easy" = {
|
||||
requires = ["wg-easy-secrets.service"];
|
||||
after = ["wg-easy-secrets.service"];
|
||||
};
|
||||
|
||||
systemd.services.wg-nat66 = {
|
||||
description = "NAT66 for WireGuard IPv6";
|
||||
after = ["network.target" "podman-wg-easy.service"];
|
||||
wants = ["podman-wg-easy.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
${pkgs.iptables}/bin/ip6tables -t nat -C POSTROUTING -s fd10:8::/64 -o enp1s0 -j MASQUERADE 2>/dev/null || \
|
||||
${pkgs.iptables}/bin/ip6tables -t nat -A POSTROUTING -s fd10:8::/64 -o enp1s0 -j MASQUERADE
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
shkeeper_api_key: ENC[AES256_GCM,data:kUymtefYJOllK5cxKHhD3HnXO/uo1w==,iv:OH9ZaQJZsFfo00BIl5MlRw1XBzHUmFi1FW54/J6O/8Q=,tag:HQJyuEoz5fsDi7iMq3PEzw==,type:str]
|
||||
restic_password: ENC[AES256_GCM,data:OHQlxUpNnTqMQm6A/o3ID/3F91NpVKOrsFYdLqrtI4vp+6TWHR8=,iv:bBd+gfi78lgTaTv0QUWYGQWPsurrzx90VvOzA2d2euA=,tag:qsHSdAHx1auwdZCgn2O5Qw==,type:str]
|
||||
b2_key_id: ENC[AES256_GCM,data:4B9rvg06baH6aNiT,iv:Qk1ToF3lMYLTrZdzpfaoGVzdiKYs492w9fzn4/TbNfQ=,tag:djxfqwgNZmRGqvteTX9G3w==,type:str]
|
||||
b2_application_key: ENC[AES256_GCM,data:xGAbBqx+6ErK7sy3FR0yza6mJU5oc5uQQGcwKtcPp1Ef4XEVd8do/wN7,iv:CojcoykDRBfvN8yqMMrPJq9mtAwxyswqXXVBKPupxDs=,tag:BkfFHAGJvtCDUpFoxXVP3Q==,type:str]
|
||||
stalwart_admin_hash: ENC[AES256_GCM,data:WqRfogOC+LrlSmg8/A4kLaUVJRdGgfrCnbuAzwyf4SMed37vsialN5QPR1IxiY02ueqz12dZ1VObittLMGxK+tUIIAwv3+F3uyKboDSyP9+wGAhBkcwTFTMjc6RMzaJ5VHW4OGaSvijiMQ==,iv:jvzoEI8evtFxuZ7W4P3nkQ0KwccVLj0j/s0s/MMXmIo=,tag:KopbV++Y/aABh1c8zfo1dw==,type:str]
|
||||
wg_admin_password: ENC[AES256_GCM,data:X/+YJoNoFFU7P/2HCpTI,iv:FXDS3xuFB9jxjpQhsX2EKn0YFVzL23c2sZO6RqpKTiw=,tag:ccyQVzqNc1HEY0rxJSLlKw==,type:str]
|
||||
virtualcam_license_signing_key: ENC[AES256_GCM,data:zk8YDER9d9mBOdXgEKKC6dubSMfz6UMMb758ZC9SvjkYIv+R2EVd+CJwL93MCs0l62lUDxoBDCgTLbKM55kcaThkkqfgZCYPrndXevYZ7kquNQEMkoYg/pyGAWhAQ9trvT5QXfBES9UDdYi6J9taCVKCofutiMEdoJM8olgUwKsP4i+fbt3idYkdWNGelastPnvTuUK/M3/dOKR5eheiy5oIntG+sECOz/2eKHJ8cU3ja18uAEwcpd9VojuOa0fvfui8AujQcMZNSMv8VsLGnoR854iX3g1MvHkTBSzv3qaDzRC54MueXYR4WOzm0o+yv6Otmw1n9trWtk+WLUimmiKxDdW6nSFaddeisWzy47+QYzrSsd7iovNYEIHnW4kpnT5W2g5lwaJwA0J0pBOqvV9gziv8oBNeEbLurBxi78KwHBqG,iv:svx+hC4tS26xqI05+yo6N2d3uHzO7ULM0e76dMbShjs=,tag:EJ9fjHEi1wUhLkxy8sexww==,type:str]
|
||||
virtualcam_admin_token: ENC[AES256_GCM,data:JC5zSFBCoEapxv/Kcs4VXk3HAkiqwXVrCv1rlqFsXVx3LGspnQdzjOIahlM=,iv:GRVfMNGkwC3xXR00ww29rFbhTrpuViad+H05yDhR9BA=,tag:OBnxBD/8KU4mn17FFTcdng==,type:str]
|
||||
git_ssh_key_b64: ENC[AES256_GCM,data:LaUcR005w9iCd/DifDhzLM7tU4AuTiFM802gMU1HMtvFN2iSffucaFJWxpKv2zFxDxz0QpA3XwM1M5yJC4K3YFMxPkPXcWLYN4yHcc1yXuoB3SseQXjcJnPMqZvJfVYhHep+6CVVlVDQs1Y+miBvbAhrvCEblwUu5LhD7yvoJLwTnEpT15WGDvqezzYKzNSyQGNRa8KPjsD/7gJN3RyUPzKo6GsrIp8d8eOTuCZwEcJVDKE0ACXs4V6cd+akqIWZkNPyilnMTuK5cqg2YuqSpwu9NncKED6SjmPiozPKKgTVaWhNCRJUTBfIw85fdXizvq7PNbNRxxBsscAWd0ixXiLqg1RTipfv85wL4Nru3r3qcQGAWmOpTHKW5fA9kR4ygdNxEVkOgQ70m9U0wev+vdIJUoRYj09Ikobej370NKppOP6+oeEDE7NpkFH7tzRk1oHtwoc1dwP9Kq1T8XdxttCKFyb1pAJ6gMdMThCfJlC9/Uu/WYbYVu5bCj6BAmrdrmhbPrsEOylBeC9j9RhyRrFEqLd8AZ01Zg8oKcj9WtCWEMf9Y4biv2xE43JbBGRvLAx3sH9MRplu2hN2W/bRxa/EPj9GHgbUjScDBkm/QVbMeV+QOgi09sCHDTeSX3BHdZ8ZLNjuNUwYnWZq4lPix0iYS5MufiixNdQmncPeHq0UuBtVifEl2Jmjq1COLyeAQe47AKQ5zDjT91xtBRZVZEVk0kGBYHtBEJ9ezPwckSw=,iv:cbIpwwhmyJVvvTMB95YBzt+RQLOji6yJh+SpcJc3Q1Y=,tag:t8WxU1IPDfkB8muTdBLUhg==,type:str]
|
||||
virtualcam_smtp_pass: ENC[AES256_GCM,data:D5kDTQP9/YV5BjAWeTn0G2zKe+s=,iv:7dZJprrs0S2ECtbDQuvhUWsU0Xzpld5/XZqdbyefg0o=,tag:uWe811KAisBJ5M1EhJjrcQ==,type:str]
|
||||
virtualcam_google_OAuth_secret: ENC[AES256_GCM,data:kPJIP4iH2vFo+VRojvhjzrQhDWidFq7kjC7ZaFcwJ7dNHlo=,iv:qKWClwSIRPU2Xz50bGYqU+eMR/iFxqnaKkb18eL3aMU=,tag:78jGQgL1myDBMiZP8GZ80g==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1yd59qp5km4cxt99rlfjehnsucrjn9lmj0su4h3avhf6vrtjvnyjqstldl5
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBycGNTU1cvbDFhMXRmb2xj
|
||||
Sm83dEdRV2Nsc28vWGwvWDRoYUxnaC9BVWdBClh0a1lLMEhIa0NGVmNQL0NTT05y
|
||||
Y1g1eU1jUEo3bFg5OWw2a1JNdkozanMKLS0tIFVXL1hueGcwQ2ZuMXg0alVxSEZM
|
||||
eDhBZ1UweEJ1UlZJdzB0RDhiRG1MeDgKLm2QHJAi++C5C86DDrl7dM0MSpYu11mn
|
||||
PhD9ElDJO9dFVCh+X+CSJwKEslR1aAraE2iQSHHgWxbMw7MB6AA9uQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age15rhqdpwejyf3r6ww70qgv6hqmkpsqraakn26kc49wlauhaceaeqsmuwrdd
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPdUQ3aVYxWElRc3ZBVENn
|
||||
L0dud1ViVi9uZHJ6NXcxN0VVd2VlNGZhUXdnClZpRWhJcC9sYjBxVk1kdkdGZmVE
|
||||
QS9LL2ZtNnJIdmhpa2NUTlJqQk5jSUkKLS0tIER2emlCbkJ1ZFdHdmdPNndZT09I
|
||||
MFhYTUpJaUhDVmlrVzNocWhRZ0t1Q00KV1rgDAOoqlzEuO7xoo2ZYL20dF3f1pCj
|
||||
bNllMkJ0u+hSZin6aUIRV31ExCYtgivDVD3Jx09PKwuYqkINU6ui8A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1xekdrkjfu82hkxltydm72tllzgayyvfavvudeks3xjuujm5wt5hq6g55v0
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuYWlFTTlHMWFaeGEvNGhz
|
||||
YVhEaVdOWXAvNTZrMjhRUWo1WUtNRWlyTGd3CkkrYnFIYnJncDdQZzdPcWR2UDJB
|
||||
TUw3ckF1eEdlTDA3SXVsdmVKMUpDamMKLS0tIGVQK2hsWUlwTUhzSG5keFROWEMz
|
||||
MHJrVVpDYWdJNmxtUkozSzR4Nmt3R28KrhYi830HUFAPfg8WvPad7BAuNe1mYOWt
|
||||
WEFIquuX/H/N+y/7uQcBDbvnBzyropE1hW8aNrxSKMeawvQZWNXkZA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-08-19T10:30:19Z"
|
||||
mac: ENC[AES256_GCM,data:JOWFguh2iE4+OyU00l+2gn7GWv2yej37OLLhVVO6Yb7Kv4vgk4NhZKJLqopteQK1o6dPeWbeIydHG1Qmef0Sm0NmCyi7i6BCTplVizf/puaGjpkU5YAs90+HiEa9cKlNQ5brfNH3YecJeN9dK+d7o8bMO/xPv3raqFMBBoVtgu4=,iv:A7kCOzyWwiVaXKVMKXQ4j86zz2f65wAjOtaVEuLa81I=,tag:bcbPsmRRFRHX7qi4WdYb0g==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
||||
+1
-1
@@ -1 +1 @@
|
||||
disabled = ["empty_pattern"]
|
||||
disabled = ["empty_pattern", "repeated_keys"]
|
||||
|
||||
+2
-1
@@ -21,9 +21,10 @@ let
|
||||
./services/pipewire.nix # audio
|
||||
];
|
||||
|
||||
# Laptop-specific modules (battery, bluetooth)
|
||||
# Laptop-specific modules (battery, bluetooth, fingerprint)
|
||||
laptop = [
|
||||
./hardware/bluetooth.nix
|
||||
./hardware/fingerprint.nix
|
||||
./services/power.nix
|
||||
];
|
||||
in {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
# Fingerprint scanner configuration
|
||||
#
|
||||
# Enable on laptops with a fingerprint reader:
|
||||
# hardware.fingerprint.enable = true;
|
||||
#
|
||||
# For some sensors (Goodix, Elan, Synaptics), you may also need to set
|
||||
# the TOD driver:
|
||||
# hardware.fingerprint.todDriver = pkgs.libfprint-2-tod1-goodix;
|
||||
#
|
||||
# Reference: https://wiki.nixos.org/wiki/Fingerprint_scanner
|
||||
let
|
||||
cfg = config.hardware.fingerprint;
|
||||
in {
|
||||
options.hardware.fingerprint = {
|
||||
enable = lib.mkEnableOption "fingerprint scanner support (fprintd)";
|
||||
|
||||
todDriver = lib.mkOption {
|
||||
description = "Touch OEM Drivers (TOD) package to use for the fingerprint sensor";
|
||||
type = lib.types.nullOr lib.types.package;
|
||||
default = null;
|
||||
example = lib.literalExpression "pkgs.libfprint-2-tod1-goodix";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.fprintd.enable = true;
|
||||
|
||||
services.fprintd.tod = lib.mkIf (cfg.todDriver != null) {
|
||||
enable = true;
|
||||
driver = cfg.todDriver;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,16 @@
|
||||
in {
|
||||
package = pkgs.lix;
|
||||
|
||||
gc = {
|
||||
automatic = true;
|
||||
dates = "weekly";
|
||||
options = "--delete-older-than 14d";
|
||||
};
|
||||
|
||||
optimise = {
|
||||
automatic = true;
|
||||
};
|
||||
|
||||
# pin the registry to avoid downloading and evaling a new nixpkgs version every time
|
||||
registry = lib.mapAttrs (_: v: {flake = v;}) flakeInputs;
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Cisco Secure Client (proprietary VPN client, successor to AnyConnect)
|
||||
#
|
||||
# Package: pkgs/cisco-secure-client — fetches the Linux pre-deployment .tgz
|
||||
# from archive.org and extracts the bundled .deb.
|
||||
#
|
||||
# The user can override the source by passing a different package:
|
||||
# work.cisco.package = pkgs.cisco-secure-client.overrideAttrs (_: {
|
||||
# src = /path/to/local/cisco-secure-client-linux64-5.1.17.3394-predeploy-deb-k9.tgz;
|
||||
# });
|
||||
#
|
||||
# Reference: https://github.com/NixOS/nixpkgs/issues/265443
|
||||
let
|
||||
cfg = config.work.cisco;
|
||||
in {
|
||||
options.work.cisco = {
|
||||
enable =
|
||||
lib.mkEnableOption "Cisco Secure Client"
|
||||
// {
|
||||
default = false;
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
description = "cisco-secure-client package to use";
|
||||
type = lib.types.package;
|
||||
default = pkgs.cisco-secure-client;
|
||||
defaultText = "pkgs.cisco-secure-client";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = with pkgs; [
|
||||
cfg.package
|
||||
];
|
||||
|
||||
# The vpnagentd binary has /opt/cisco/secureclient/ hardcoded for runtime
|
||||
# config paths (openssl.cnf, ossl-modules/). Create a symlink to the Nix store.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /opt 0755 root root -"
|
||||
"L+ /opt/cisco/secureclient - - - - ${cfg.package}/cisco/secureclient"
|
||||
];
|
||||
|
||||
# Load the tun module required by the VPN client
|
||||
boot.kernelModules = ["tun"];
|
||||
|
||||
systemd.services.cisco-vpnagentd = {
|
||||
description = "Cisco Secure Client VPN Agent Daemon";
|
||||
# vpnagentd daemonizes itself; tracked via PID file.
|
||||
after = ["network-online.target" "NetworkManager.service"];
|
||||
wants = ["network-online.target"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
PIDFile = "/run/vpnagentd.pid";
|
||||
# Use the wrapper (sets LD_LIBRARY_PATH) — the raw binary was restored
|
||||
# from backup to preserve its embedded Cisco code signature, so it has
|
||||
# no RPATH and needs the env var to find its bundled .so files.
|
||||
ExecStart = "${cfg.package}/bin/vpnagentd";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5";
|
||||
|
||||
# Minimal capability set for VPN operation
|
||||
CapabilityBoundingSet = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_NET_RAW"
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
"CAP_DAC_OVERRIDE"
|
||||
"CAP_SETUID"
|
||||
"CAP_SETGID"
|
||||
"CAP_CHOWN"
|
||||
"CAP_FOWNER"
|
||||
];
|
||||
AmbientCapabilities = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_NET_RAW"
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
"CAP_DAC_OVERRIDE"
|
||||
"CAP_SETUID"
|
||||
"CAP_SETGID"
|
||||
"CAP_CHOWN"
|
||||
"CAP_FOWNER"
|
||||
];
|
||||
DeviceAllow = ["/dev/net/tun rw"];
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "full";
|
||||
ProtectHome = false;
|
||||
NoNewPrivileges = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./overlay.nix
|
||||
./cisco.nix
|
||||
./himmelblau.nix
|
||||
./mdatp.nix
|
||||
];
|
||||
|
||||
# Create the work directory for the user
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /home/someone/work 0755 someone users -"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# Himmelblau: Microsoft Entra ID authentication for Linux
|
||||
#
|
||||
# Authenticates Linux users against the digistate.nl Entra ID tenant.
|
||||
# Users log in with their Entra ID credentials via OIDC Device Authorization
|
||||
# Grant flow (browser-based) or the native PAM orchestrator.
|
||||
#
|
||||
# Requires:
|
||||
# - A working Entra ID tenant with digistate.nl as a verified domain
|
||||
# - An OIDC app registration (Himmelblau client) in the tenant
|
||||
# - Network connectivity to login.microsoftonline.com
|
||||
#
|
||||
# References:
|
||||
# - https://himmelblau-idm.org/docs/
|
||||
# - https://github.com/himmelblau-idm/himmelblau
|
||||
let
|
||||
cfg = config.work.himmelblau;
|
||||
in {
|
||||
imports = [
|
||||
inputs.himmelblau.nixosModules.himmelblau
|
||||
];
|
||||
|
||||
options.work.himmelblau = {
|
||||
enable =
|
||||
lib.mkEnableOption "Himmelblau Entra ID authentication"
|
||||
// {
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.himmelblau = {
|
||||
enable = true;
|
||||
settings = {
|
||||
domain = ["digistate.nl"];
|
||||
# Uncomment and set to Entra ID group Object IDs or names to
|
||||
# restrict which users can authenticate:
|
||||
# pam_allow_groups = [ "ENTRA-GROUP-GUID-HERE" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Himmelblau registers an NSS module (system.nssModules) for user/group lookups,
|
||||
# which requires nscd to be enabled. Keep it on.
|
||||
# services.nscd.enable = lib.mkForce false;
|
||||
|
||||
# Expose the aad-tool CLI for diagnostics and enrollment
|
||||
environment.systemPackages = [
|
||||
inputs.himmelblau.packages.${pkgs.system}.aad-tool
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# Microsoft Defender for Endpoint
|
||||
#
|
||||
# References:
|
||||
# - https://github.com/epetousis/nix-mdatp
|
||||
let
|
||||
cfg = config.work.mdatp;
|
||||
in {
|
||||
imports = [
|
||||
inputs.mdatp.nixosModules.mdatp
|
||||
];
|
||||
|
||||
options.work.mdatp = {
|
||||
enable = lib.mkEnableOption "Microsoft Defender for Endpoint";
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.mdatp.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{...}: {
|
||||
# Overlay to add cisco-secure-client to pkgs
|
||||
nixpkgs.overlays = [
|
||||
(_self: super: {
|
||||
cisco-secure-client = super.callPackage ./pkgs/cisco-secure-client {};
|
||||
})
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user