diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..a2c7b59 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,25 @@ +# To set up sops-nix: +# 1. Generate an age key on each host: +# mkdir -p ~/.config/sops/age +# age-keygen -o ~/.config/sops/age/keys.txt +# Or derive from the host SSH key: +# nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' +# +# 2. Replace the placeholder age keys below with the actual public keys. +# +# 3. Encrypt secret files: +# sops secrets/homelab.yaml +# +# 4. To re-key after changing keys: +# sops updatekeys secrets/homelab.yaml + +keys: + - &homelab age1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # replace with: ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub + - &admin age1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # replace with: age-keygen output from your admin machine + +creation_rules: + - path_regex: secrets/homelab\.yaml$ + key_groups: + - age: + - *homelab + - *admin \ No newline at end of file diff --git a/modules/system/homelab/auth.nix b/modules/system/homelab/auth.nix index 57823b9..62e4f39 100644 --- a/modules/system/homelab/auth.nix +++ b/modules/system/homelab/auth.nix @@ -1,7 +1,7 @@ -{ homelab, ... }: { +{ config, homelab, ... }: { services.pocket-id = { enable = true; - credentials.ENCRYPTION_KEY = "/mnt/data/pocketid/encryption-key"; + credentials.ENCRYPTION_KEY = config.sops.secrets.pocketid_encryption_key.path; dataDir = "/mnt/data/pocketid/data"; settings = { PORT = "1411"; diff --git a/modules/system/homelab/dash.nix b/modules/system/homelab/dash.nix index 3923242..a27e591 100644 --- a/modules/system/homelab/dash.nix +++ b/modules/system/homelab/dash.nix @@ -1,4 +1,4 @@ -{ timezone, homelab, ... }: let +{ config, timezone, homelab, ... }: let rss = [ "https://www.raspberrypi.com/news/feed/" "https://www.jeffgeerling.com/blog.xml" @@ -96,7 +96,7 @@ in { }; services.glance = { enable = true; - environmentFile = "/var/lib/glance/.env"; + environmentFile = config.sops.secrets.glance_env.path; settings = { server = { host = "127.0.0.1"; diff --git a/modules/system/homelab/pass.nix b/modules/system/homelab/pass.nix index 89ea489..d3f5805 100644 --- a/modules/system/homelab/pass.nix +++ b/modules/system/homelab/pass.nix @@ -1,9 +1,9 @@ -{ homelab, ... }: { +{ config, homelab, ... }: { services.vaultwarden = { enable = true; domain = "pass.proxy.${homelab.domain}"; backupDir = "/mnt/data/vaultwarden/backups"; - environmentFile = "/mnt/data/vaultwarden/.env"; + environmentFile = config.sops.secrets.vaultwarden_env.path; config = { ROCKET_PORT = 8060; ROCKET_ADDRESS = "127.0.0.1"; diff --git a/modules/system/homelab/proxy.nix b/modules/system/homelab/proxy.nix index 85671a5..37909e7 100644 --- a/modules/system/homelab/proxy.nix +++ b/modules/system/homelab/proxy.nix @@ -1,4 +1,4 @@ -{ homelab, lib, ... }: let +{ config, homelab, lib, ... }: let base = "proxy.${homelab.domain}"; hosts = { "server" = { dest = "https://server.dns.${homelab.domain}:8006"; auth = false; }; @@ -45,8 +45,7 @@ in { domain = "*.${base}"; extraDomainNames = [ base ]; dnsProvider = "cloudflare"; - environmentFile = "/var/lib/acme/cloudflare.env"; - # ^^^contents: CLOUDFLARE_DNS_API_TOKEN=XXXXX + environmentFile = config.sops.templates."cloudflare.env".path; }; }; @@ -81,7 +80,7 @@ in { locations."/" = { proxyPass = cfg.dest; proxyWebsockets = true; - basicAuthFile = if cfg.auth then "/var/lib/nginx/.htpasswd" else null; + basicAuthFile = if cfg.auth then config.sops.secrets.nginx_htpasswd.path else null; extraConfig = exta-conf; }; }) hosts; diff --git a/modules/system/homelab/sops.nix b/modules/system/homelab/sops.nix new file mode 100644 index 0000000..4ce61b7 --- /dev/null +++ b/modules/system/homelab/sops.nix @@ -0,0 +1,59 @@ +{ config, ... }: { + sops = { + defaultSopsFile = ../../../secrets/homelab.yaml; + age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + + secrets = { + cloudflare_dns_api_token = { + owner = "acme"; + group = "acme"; + }; + + cloudflared_tunnel_credentials = { + owner = "cloudflared"; + group = "cloudflared"; + }; + + cloudflared_cert = { + owner = "cloudflared"; + group = "cloudflared"; + }; + + vaultwarden_env = { + owner = "vaultwarden"; + group = "vaultwarden"; + restartUnits = [ "vaultwarden.service" ]; + }; + + glance_env = { + owner = "glance"; + group = "glance"; + restartUnits = [ "glance.service" ]; + }; + + pocketid_encryption_key = { + owner = "root"; + group = "root"; + restartUnits = [ "pocket-id.service" ]; + }; + + tailscale_authkey = { + owner = "root"; + group = "root"; + restartUnits = [ "tailscaled.service" ]; + }; + + nginx_htpasswd = { + owner = "nginx"; + group = "nginx"; + restartUnits = [ "nginx.service" ]; + }; + }; + + templates."cloudflare.env" = { + owner = "acme"; + group = "acme"; + content = "CLOUDFLARE_DNS_API_TOKEN=${config.sops.placeholder.cloudflare_dns_api_token}"; + }; + }; +} \ No newline at end of file diff --git a/modules/system/homelab/tunnels.nix b/modules/system/homelab/tunnels.nix index 8cf0fb6..4c5402a 100644 --- a/modules/system/homelab/tunnels.nix +++ b/modules/system/homelab/tunnels.nix @@ -1,4 +1,4 @@ -{ pkgs, lib, homelab, ... }: let +{ config, pkgs, lib, homelab, ... }: let routes = { "git.${homelab.domain}" = "http://localhost:5080"; "auth.${homelab.domain}" = "http://localhost:1411"; @@ -10,8 +10,8 @@ in { services.cloudflared = { enable = true; tunnels.homelab = { - credentialsFile = "/mnt/data/cloudflared/homelab.json"; - certificateFile = "/mnt/data/cloudflared/cert.pem"; + credentialsFile = config.sops.secrets.cloudflared_tunnel_credentials.path; + certificateFile = config.sops.secrets.cloudflared_cert.path; default = "http_status:404"; ingress = routes; }; @@ -31,7 +31,7 @@ in { script = lib.concatMapStringsSep "\n" (domain: '' echo "Ensuring DNS route for ${domain}..." - ${pkgs.cloudflared}/bin/cloudflared tunnel --origincert /mnt/data/cloudflared/cert.pem route dns ${homelab.cf-tunnel-id} ${domain} || true + ${pkgs.cloudflared}/bin/cloudflared tunnel --origincert ${config.sops.secrets.cloudflared_cert.path} route dns ${homelab.cf-tunnel-id} ${domain} || true '') (builtins.attrNames routes); }; } diff --git a/modules/system/server.nix b/modules/system/server.nix index fc7e686..0b1e3d9 100644 --- a/modules/system/server.nix +++ b/modules/system/server.nix @@ -1,4 +1,4 @@ -{ lib, homelab, ... }: let +{ config, lib, homelab, ... }: let ts-flags = [ "--advertise-exit-node" "--advertise-routes=10.3.14.0/24,192.168.1.0/24" @@ -20,6 +20,7 @@ in { ./homelab/dns.nix ./homelab/git.nix ./homelab/ai.nix + ./homelab/sops.nix ./core/swapfile.nix ./core/oom.nix @@ -29,7 +30,7 @@ in { services.tailscale = { enable = true; - authKeyFile = "/mnt/data/tailscale/authkey"; + authKeyFile = config.sops.secrets.tailscale_authkey.path; useRoutingFeatures = "server"; extraUpFlags = ts-flags; extraSetFlags = ts-flags; diff --git a/scripts/check-sops.sh b/scripts/check-sops.sh new file mode 100755 index 0000000..dd4daf2 --- /dev/null +++ b/scripts/check-sops.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Pre-commit hook: block commits containing unencrypted sops secret files. +# Install with: ln -sf ../../scripts/check-sops.sh .git/hooks/pre-commit + +set -euo pipefail + +staged_secrets=$(git diff --cached --name-only --diff-filter=ACM -- 'secrets/*.yaml' 'secrets/*.yml' 'secrets/*.json') + +if [ -z "$staged_secrets" ]; then + exit 0 +fi + +failed=0 + +for file in $staged_secrets; do + # sops-encrypted YAML/JSON files always contain a top-level "sops" key with metadata + if ! git show ":$file" | grep -q '"sops"\|sops:'; then + echo "ERROR: $file is not encrypted with sops! Encrypt it first:" + echo " sops $file" + echo + echo "hint: bypass with: git commit --no-verify" + failed=1 + fi +done + +if [ "$failed" -ne 0 ]; then + echo "" + echo "Commit aborted. Encrypt secret files before committing." + exit 1 +fi \ No newline at end of file diff --git a/secrets/homelab.yaml b/secrets/homelab.yaml new file mode 100644 index 0000000..93cb53a --- /dev/null +++ b/secrets/homelab.yaml @@ -0,0 +1,11 @@ +# This file should be encrypted with sops before committing. +# Run: sops secrets/homelab.yaml +# All values below are placeholders. Replace them with actual values. +cloudflare_dns_api_token: REPLACE_ME +cloudflared_tunnel_credentials: REPLACE_ME +cloudflared_cert: REPLACE_ME +vaultwarden_env: REPLACE_ME +glance_env: REPLACE_ME +pocketid_encryption_key: REPLACE_ME +tailscale_authkey: REPLACE_ME +nginx_htpasswd: REPLACE_ME