diff --git a/backup-monitoring.nix b/backup-monitoring.nix new file mode 100644 index 0000000..f27bb9f --- /dev/null +++ b/backup-monitoring.nix @@ -0,0 +1,54 @@ +{ config, pkgs, lib, ... }: +{ + # Database backup systemd timer + systemd.services.headscale-backup = { + description = "Backup Headscale database"; + serviceConfig = { + Type = "oneshot"; + User = "root"; + }; + script = '' + backup_dir="/var/backups/headscale" + mkdir -p $backup_dir + timestamp=$(date +%Y%m%d_%H%M%S) + ${pkgs.sqlite}/bin/sqlite3 /var/lib/headscale/db.sqlite ".backup '$backup_dir/headscale_$timestamp.sqlite'" + # Keep only last 7 backups + cd $backup_dir && ls -t | tail -n +8 | xargs -r rm + echo "Backup completed: $backup_dir/headscale_$timestamp.sqlite" + ''; + }; + + systemd.timers.headscale-backup = { + description = "Daily Headscale database backup timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + }; + }; + + # /boot disk space monitoring + systemd.services.boot-disk-check = { + description = "Check /boot disk space"; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + usage=$(${pkgs.coreutils}/bin/df /boot | ${pkgs.gawk}/bin/awk 'NR==2 {print $5}' | ${pkgs.gnused}/bin/sed 's/%//') + if [ "$usage" -gt 80 ]; then + echo "WARNING: /boot is $usage% full!" + # Log to journal for monitoring + ${pkgs.systemd}/bin/systemd-cat -t boot-disk-check -p warning echo "/boot disk usage is at $usage%" + fi + ''; + }; + + systemd.timers.boot-disk-check = { + description = "Check /boot disk space daily"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + }; + }; +} diff --git a/configuration.nix b/configuration.nix index dde093e..51b46e1 100644 --- a/configuration.nix +++ b/configuration.nix @@ -34,14 +34,25 @@ server = { host = "127.0.0.1"; port = 3000; - cookie_secret = "iQ0bUyaFgwaijWaSyZ1ILA9RwfywrbZ3"; - cookie_secure = false; + base_url = "https://headplane.kennys.mom"; + cookie_secret = "YgNMqokyzieAMLD5ASHWCgs9vBB07kxQ"; + cookie_secure = true; }; headscale = { url = "https://headscale.kennys.mom"; config_path = "/etc/headscale-strict.yml"; config_strict = true; }; + oidc = { + enabled = true; + issuer = "https://auth.kennys.mom/realms/headscale"; + client_id = "headplane"; + client_secret = "4MESLzCyNdSo91QH9hMtSMtpZgazAqtw"; + redirect_uri = "https://headplane.kennys.mom/admin/oidc/callback"; + token_endpoint_auth_method = "client_secret_post"; + disable_api_key_login = false; + headscale_api_key = "M65sVHh.3PeukofDrstuoIcDGhFaCuskZcMIW9CD"; + }; }; agent.enable = false; }; @@ -61,6 +72,34 @@ }; }; + # Create headplane data directory + systemd.services.headplane-setup = { + description = "Create Headplane data directory"; + wantedBy = [ "multi-user.target" ]; + before = [ "headplane.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p /var/lib/headplane + chmod 777 /var/lib/headplane + if [ ! -f /var/lib/headplane/users.json ]; then + echo "{}" > /var/lib/headplane/users.json + chmod 666 /var/lib/headplane/users.json + fi + ''; + }; + + # Automatic cleanup to prevent /boot from filling up + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + + boot.loader.grub.configurationLimit = 10; + system.stateVersion = "23.11"; diff --git a/flake.nix b/flake.nix index ac6e0bf..4080c11 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,10 @@ ./configuration.nix ./hardware-configuration.nix ./headscale.nix + ./backup-monitoring.nix + ./keycloak.nix + ./oidc-secret.nix + # ./oidc.nix # Disabled - using Keycloak instead headplane.nixosModules.headplane ({ pkgs, ... }: { nixpkgs.overlays = [ diff --git a/headscale.nix b/headscale.nix index afd448a..e669a79 100644 --- a/headscale.nix +++ b/headscale.nix @@ -40,8 +40,9 @@ in enabled = false; }; log = { - level = "warn"; + level = "info"; }; + node_update_check_interval = "10s"; derp.server = { enable = true; region_id = 999; @@ -54,6 +55,12 @@ in grpc_listen_addr = "127.0.0.1:50443"; # Required for Headplane communication api_key_path = "/etc/headscale/apikey"; policy.mode = "database"; + oidc = { + issuer = "https://auth.kennys.mom/realms/headscale"; + client_id = "headplane"; + client_secret_path = "/var/lib/headscale/oidc_client_secret"; + strip_email_domain = true; + }; }; }; # Put strict config as file for headplane diff --git a/keycloak-headscale-realm.json b/keycloak-headscale-realm.json new file mode 100644 index 0000000..12ceb3c --- /dev/null +++ b/keycloak-headscale-realm.json @@ -0,0 +1,128 @@ +{ + "id": "headscale", + "realm": "headscale", + "displayName": "Headscale", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 5, + "defaultSignatureAlgorithm": "RS256", + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clients": [ + { + "clientId": "headplane", + "name": "Headplane Web UI", + "description": "Headscale web administration interface", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**************", + "redirectUris": [ + "https://headplane.kennys.mom/*" + ], + "webOrigins": [ + "https://headplane.kennys.mom" + ], + "protocol": "openid-connect", + "attributes": {}, + "fullScopeAllowed": true, + "publicClient": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": true, + "alwaysDisplayInConsole": false, + "rootUrl": "https://headplane.kennys.mom", + "baseUrl": "/admin" + } + ], + "clientScopes": [ + { + "name": "openid", + "protocol": "openid-connect", + "attributes": {}, + "protocolMappers": [] + }, + { + "name": "profile", + "protocol": "openid-connect", + "attributes": {}, + "protocolMappers": [ + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + } + ] + }, + { + "name": "email", + "protocol": "openid-connect", + "attributes": {}, + "protocolMappers": [ + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "users": [], + "roles": { + "realm": [ + { + "name": "user", + "description": "Standard Headscale user" + }, + { + "name": "admin", + "description": "Headscale administrator" + } + ] + } +} \ No newline at end of file diff --git a/keycloak.nix b/keycloak.nix new file mode 100644 index 0000000..cfc4e35 --- /dev/null +++ b/keycloak.nix @@ -0,0 +1,90 @@ +{ config, pkgs, lib, ... }: +let + domain = "kennys.mom"; + authDomain = "auth.${domain}"; +in +{ + # PostgreSQL for Keycloak + services.postgresql = { + enable = true; + ensureDatabases = [ "keycloak" ]; + ensureUsers = [{ + name = "keycloak"; + ensureDBOwnership = true; + }]; + }; + + # Keycloak service + services.keycloak = { + enable = true; + database = { + type = "postgresql"; + username = "keycloak"; + name = "keycloak"; + host = "localhost"; + createLocally = false; + passwordFile = "/var/lib/keycloak-credentials/db-password"; + }; + settings = { + hostname = authDomain; + http-host = "127.0.0.1"; + http-port = 8080; + http-enabled = true; + proxy-headers = "xforwarded"; + hostname-strict-https = false; + }; + }; + + # Generate database password and use LoadCredential + systemd.services.keycloak = { + serviceConfig = { + LoadCredential = [ "db-password:/var/lib/keycloak-credentials/db-password" ]; + TimeoutStartSec = "5min"; + }; + }; + + systemd.services.keycloak-db-password = { + description = "Generate Keycloak database password"; + wantedBy = [ "multi-user.target" ]; + before = [ "keycloak.service" ]; + after = [ "postgresql.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p /var/lib/keycloak-credentials + + if [ ! -f /var/lib/keycloak-credentials/db-password ]; then + echo "Generating Keycloak database password..." + DB_PASS=$(${pkgs.openssl}/bin/openssl rand -base64 32) + echo -n "$DB_PASS" > /var/lib/keycloak-credentials/db-password + chmod 600 /var/lib/keycloak-credentials/db-password + + # Set the password in PostgreSQL + ${pkgs.sudo}/bin/sudo -u postgres ${config.services.postgresql.package}/bin/psql -c "ALTER USER keycloak WITH PASSWORD '$DB_PASS';" + + echo "Database password set" + fi + ''; + }; + + # Ngin configuration for Keycloak + services.nginx.virtualHosts."${authDomain}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8080"; + extraConfig = '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + ''; + }; + }; +} diff --git a/oidc-secret.nix b/oidc-secret.nix new file mode 100644 index 0000000..d765751 --- /dev/null +++ b/oidc-secret.nix @@ -0,0 +1,23 @@ +{ config, pkgs, lib, ... }: +{ + # Store Keycloak client secret + systemd.services.headscale-oidc-secret = { + description = "Create Headscale OIDC client secret"; + wantedBy = [ "multi-user.target" ]; + before = [ "headscale.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p /var/lib/headscale + if [ ! -f /var/lib/headscale/oidc_client_secret ]; then + echo -n "4MESLzCyNdSo91QH9hMtSMtpZgazAqtw" > /var/lib/headscale/oidc_client_secret + echo "OIDC client secret created" + fi + # Always fix permissions + chmod 640 /var/lib/headscale/oidc_client_secret + chown headscale:headscale /var/lib/headscale/oidc_client_secret + ''; + }; +}