Runner updated and eurovote

This commit is contained in:
Aner Zakobar
2026-05-20 11:12:32 +03:00
parent d2793904f4
commit 42d91012c1
12 changed files with 170 additions and 21 deletions
Generated
+21
View File
@@ -1,5 +1,25 @@
{ {
"nodes": { "nodes": {
"eurovote": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778959671,
"narHash": "sha256-MR70Q1lNOX7lqO7PwQUtJdB4+exZr8R10YPQanc5SwE=",
"owner": "anerisgreat",
"repo": "eurovote",
"rev": "245d9b1f3e182653e5cfa0d9689a97f263eb4354",
"type": "github"
},
"original": {
"owner": "anerisgreat",
"repo": "eurovote",
"type": "github"
}
},
"nixos-hardware": { "nixos-hardware": {
"locked": { "locked": {
"lastModified": 1776983936, "lastModified": 1776983936,
@@ -34,6 +54,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"eurovote": "eurovote",
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"sops-nix": "sops-nix" "sops-nix": "sops-nix"
+9 -1
View File
@@ -14,9 +14,15 @@
# We use only the minimal pieces needed for a headless server — # We use only the minimal pieces needed for a headless server —
# no display, audio, or bluetooth modules. # no display, audio, or bluetooth modules.
nixos-hardware.url = "github:NixOS/nixos-hardware/master"; nixos-hardware.url = "github:NixOS/nixos-hardware/master";
# Eurovision voting app
eurovote = {
url = "github:anerisgreat/eurovote";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, sops-nix, nixos-hardware, ... }@inputs: outputs = { self, nixpkgs, sops-nix, nixos-hardware, eurovote, ... }@inputs:
let let
# Shared specialArgs passed to every host # Shared specialArgs passed to every host
commonArgs = { commonArgs = {
@@ -74,6 +80,8 @@
./modules/services/uptime-kuma.nix ./modules/services/uptime-kuma.nix
./modules/services/ntfy.nix ./modules/services/ntfy.nix
./modules/monitoring.nix ./modules/monitoring.nix
eurovote.nixosModules.default
./modules/services/eurovote.nix
] ++ extraModules; ] ++ extraModules;
}; };
+14 -1
View File
@@ -95,6 +95,9 @@
# CI/CD # CI/CD
homey.giteaRunner.enable = true; homey.giteaRunner.enable = true;
# Eurovision voting app
homey.eurovote.enable = true;
# Monitoring stack # Monitoring stack
homey.uptimeKuma.enable = true; homey.uptimeKuma.enable = true;
homey.ntfy.enable = true; homey.ntfy.enable = true;
@@ -186,7 +189,17 @@
# hdparm -B udev rule removed: USB-SATA bridges often don't support APM # hdparm -B udev rule removed: USB-SATA bridges often don't support APM
# commands and hdparm can hang indefinitely, causing boot-time crashes. # commands and hdparm can hang indefinitely, causing boot-time crashes.
environment.systemPackages = [ pkgs.hdparm ]; environment.systemPackages = [ pkgs.hdparm pkgs.tmux ];
systemd.services.nextcloud-generate-previews = {
description = "Generate missing Nextcloud preview thumbnails";
after = [ "podman-nextcloud.service" ];
requires = [ "podman-nextcloud.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.podman}/bin/podman exec -u www-data nextcloud php occ preview:generate-all";
};
};
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Local DNS overrides (optional — makes LAN clients hit the Pi directly # Local DNS overrides (optional — makes LAN clients hit the Pi directly
+3
View File
@@ -139,12 +139,15 @@ in
# Monitoring — uptime-kuma has monitors/history, ntfy has user accounts # Monitoring — uptime-kuma has monitors/history, ntfy has user accounts
"${dataDir}/uptime-kuma" "${dataDir}/uptime-kuma"
"${dataDir}/ntfy" "${dataDir}/ntfy"
# Eurovision Vote — SQLite DB with votes and rankings
"/var/lib/eurovote"
]; ];
# Exclude Nextcloud's raw DB directory in favour of the pg_dump file # Exclude Nextcloud's raw DB directory in favour of the pg_dump file
exclude = [ exclude = [
"${dataDir}/nextcloud/db" "${dataDir}/nextcloud/db"
"${dataDir}/restic-cache" "${dataDir}/restic-cache"
"${dataDir}/media"
]; ];
timerConfig = { timerConfig = {
+27 -6
View File
@@ -42,19 +42,16 @@ let
# Reusable Authelia forward_auth snippet # Reusable Authelia forward_auth snippet
# Returns a Caddyfile snippet block that applies forward_auth. # Returns a Caddyfile snippet block that applies forward_auth.
# Uses the v4.38+ /api/authz/forward-auth endpoint which correctly honours
# one_factor policy without forcing TOTP enrollment on new users.
# copy_headers makes Authelia's Remote-* headers available downstream. # copy_headers makes Authelia's Remote-* headers available downstream.
autheliaForwardAuth = '' autheliaForwardAuth = ''
forward_auth localhost:9091 { forward_auth localhost:9091 {
uri /api/verify?rd=https://auth.${domain} uri /api/authz/forward-auth?authelia_url=https://auth.${domain}
copy_headers Remote-User Remote-Name Remote-Groups Remote-Email copy_headers Remote-User Remote-Name Remote-Groups Remote-Email
# Always tell Authelia the scheme is https (cloudflared terminates TLS # Always tell Authelia the scheme is https (cloudflared terminates TLS
# externally; Caddy's http:// vhosts are only for the tunnel loopback). # externally; Caddy's http:// vhosts are only for the tunnel loopback).
header_up X-Forwarded-Proto https header_up X-Forwarded-Proto https
# On auth failure, redirect to the authelia login page
@goauth status 401
handle_response @goauth {
redir https://auth.${domain}?rm={method} 302
}
} }
''; '';
@@ -236,6 +233,30 @@ in
extraConfig = cfProxy 2586; extraConfig = cfProxy 2586;
}; };
# ------------------------------------------------------------------
# Eurovision Vote — one_factor for all authenticated users.
# /admin/* is restricted to group:admins by Authelia access_control.
# Caddy passes Remote-User → X-Remote-User so Django auto-logs in
# the SSO-authenticated user via RemoteUserMiddleware.
# ------------------------------------------------------------------
"eurovision-vote.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8007 {
header_up X-Remote-User {http.request.header.Remote-User}
}
'';
};
"http://eurovision-vote.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8007 {
header_up X-Forwarded-Proto https
header_up X-Remote-User {http.request.header.Remote-User}
}
'';
};
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Grafana — two_factor, admins only (enforced by authelia policy). # Grafana — two_factor, admins only (enforced by authelia policy).
# After Authelia verifies the user, Caddy maps the Remote-User header # After Authelia verifies the user, Caddy maps the Remote-User header
+7
View File
@@ -26,6 +26,9 @@
# between weekly GC runs. # between weekly GC runs.
min-free = 2147483648; # 2 GiB min-free = 2147483648; # 2 GiB
max-free = 5368709120; # 5 GiB max-free = 5368709120; # 5 GiB
# Use the external drive for sandbox builds — the default /tmp is a
# small RAM-backed tmpfs that fills up during large builds (e.g. wrangler).
build-dir = "/mnt/data/nix-build";
}; };
gc = { gc = {
automatic = true; automatic = true;
@@ -37,6 +40,10 @@
# Allow unfree packages (e.g. cloudflared binary) # Allow unfree packages (e.g. cloudflared binary)
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
systemd.tmpfiles.rules = [
"d /mnt/data/nix-build 0755 root root -"
];
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Boot — set in hardware.nix; this is just a safe default # Boot — set in hardware.nix; this is just a safe default
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
+16
View File
@@ -128,6 +128,22 @@ let
- domain: - domain:
- "ntfy.${domain}" - "ntfy.${domain}"
policy: "bypass" policy: "bypass"
# Eurovision Vote: /admin/* for admins only; all others one_factor
- domain:
- "eurovision-vote.${domain}"
resources:
- "^/admin.*$"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "eurovision-vote.${domain}"
resources:
- "^/admin.*$"
policy: "deny"
- domain:
- "eurovision-vote.${domain}"
policy: "one_factor"
notifier: notifier:
filesystem: filesystem:
+60
View File
@@ -0,0 +1,60 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Eurovision Vote — Django app for ranking Eurovision performances.
#
# Uses the NixOS module from the eurovote flake (eurovote.nixosModules.default).
# This wrapper wires it into the homey module system: enable flag, sops secret,
# and uptime monitoring.
#
# The app uses DynamicUser + StateDirectory so systemd owns /var/lib/eurovote/;
# no tmpfiles.rules entry needed.
#
# Authentication: Caddy forward_auth → Authelia; the app reads the
# X-Remote-User header set by Caddy (from Authelia's Remote-User).
# All authenticated users get app access; /admin/* is restricted to
# group:admins by Authelia's access_control rules (see authelia.nix).
#
# Secrets consumed from sops:
# eurovote/secret_key
let
cfg = config.homey.eurovote;
domain = homeyConfig.domain;
in
{
options.homey.eurovote = {
enable = lib.mkEnableOption "Eurovision Vote app";
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
# mode 0444: the service runs as a DynamicUser (random UID) so it cannot
# read a root-owned 0400 file. /run/secrets/ itself is not world-listable
# (mode 0751), so world-readable here is acceptable on a home server.
sops.secrets."eurovote/secret_key" = { owner = "root"; mode = "0444"; };
# -----------------------------------------------------------------------
# Service (options provided by eurovote.nixosModules.default)
# -----------------------------------------------------------------------
services.eurovote = {
enable = true;
port = 8007;
allowedHosts = "localhost 127.0.0.1 eurovision-vote.${domain}";
secretKeyFile = config.sops.secrets."eurovote/secret_key".path;
trustedOrigins = "https://eurovision-vote.${domain}";
# After SSO logout, send the user back to Authelia's logout page
logoutRedirectUrl = "https://auth.${domain}/logout";
};
# -----------------------------------------------------------------------
# Uptime Kuma monitor
# -----------------------------------------------------------------------
homey.monitoring.monitors = [{
name = "Eurovision Vote";
url = "https://eurovision-vote.${domain}";
interval = 60;
}];
};
}
+8 -4
View File
@@ -8,8 +8,8 @@
# nested containers on a Pi 4. # nested containers on a Pi 4.
# #
# The service uses DynamicUser=true so there is no persistent system user. # The service uses DynamicUser=true so there is no persistent system user.
# nix/git/bash are available in jobs via the system PATH inherited from the # Job step PATH is controlled by hostPackages (not the service PATH).
# service environment. # nix is not in the NixOS module's default hostPackages and must be added.
# #
# Setup (one-time): # Setup (one-time):
# 1. In Gitea: Site Administration → Actions → Runners → Create Runner Token # 1. In Gitea: Site Administration → Actions → Runners → Create Runner Token
@@ -61,8 +61,12 @@ in
tokenFile = config.sops.secrets."gitea/runner_token".path; tokenFile = config.sops.secrets."gitea/runner_token".path;
name = cfg.name; name = cfg.name;
labels = cfg.labels; labels = cfg.labels;
# nix/git/bash are accessible via the system PATH (/run/current-system/sw/bin/) # hostPackages controls the PATH available to job steps (host executor).
# without any extra configuration — the runner inherits it as a system user. # nix is not in the default list so must be added explicitly.
hostPackages = with pkgs; [
bash coreutils curl gawk gitMinimal gnused nodejs wget
nix
];
}; };
}; };
} }
-7
View File
@@ -130,11 +130,6 @@ let
api.disconnect() api.disconnect()
sys.exit(1) sys.exit(1)
# Collect all configured notification IDs so every monitor gets them.
notification_ids = [n["id"] for n in api.get_notifications()]
if notification_ids:
print(f"uptime-kuma-sync: attaching notifications: {notification_ids}")
# Sync monitors: add missing, update changed # Sync monitors: add missing, update changed
try: try:
existing = {m["name"]: m for m in api.get_monitors()} existing = {m["name"]: m for m in api.get_monitors()}
@@ -150,7 +145,6 @@ let
url=m["url"], url=m["url"],
interval=m.get("interval", 60), interval=m.get("interval", 60),
maxretries=maxretries, maxretries=maxretries,
notification_id_list={str(nid): True for nid in notification_ids},
**extra, **extra,
) )
print(f"uptime-kuma-sync: created monitor: {m['name']}") print(f"uptime-kuma-sync: created monitor: {m['name']}")
@@ -163,7 +157,6 @@ let
url=m["url"], url=m["url"],
interval=m.get("interval", 60), interval=m.get("interval", 60),
maxretries=maxretries, maxretries=maxretries,
notification_id_list={str(nid): True for nid in notification_ids},
**extra, **extra,
) )
print(f"uptime-kuma-sync: updated monitor: {m['name']}") print(f"uptime-kuma-sync: updated monitor: {m['name']}")
+4 -2
View File
@@ -31,6 +31,8 @@ restic:
s3_secret_access_key: ENC[AES256_GCM,data:9ZWyhGJm4t2benDrLmnyQ9ZA5Jjl6l+pza1VmymTlw==,iv:xYsG6QlxXhQNO9szmsycxP6lT0cFF7lq3iNg6j+ED0E=,tag:wOJT4Vg3DuNFWTtx3QS9IQ==,type:str] s3_secret_access_key: ENC[AES256_GCM,data:9ZWyhGJm4t2benDrLmnyQ9ZA5Jjl6l+pza1VmymTlw==,iv:xYsG6QlxXhQNO9szmsycxP6lT0cFF7lq3iNg6j+ED0E=,tag:wOJT4Vg3DuNFWTtx3QS9IQ==,type:str]
wifi: wifi:
psk: ENC[AES256_GCM,data:znk9Wr+vsntzbJ3H0TORUrAiDw==,iv:wbl8fUuKlgTqhajwjlTgFS7ijaTwXBFPRW2AmtiTklg=,tag:IK4oe8cJcccPaQ0V0NlncQ==,type:str] psk: ENC[AES256_GCM,data:znk9Wr+vsntzbJ3H0TORUrAiDw==,iv:wbl8fUuKlgTqhajwjlTgFS7ijaTwXBFPRW2AmtiTklg=,tag:IK4oe8cJcccPaQ0V0NlncQ==,type:str]
eurovote:
secret_key: ENC[AES256_GCM,data:Re9MTYA46ERXsxucT19K4Pj3rV5i74s8zQ/WYj6GlxeoN1r0Oit6PP0C3PY5Arp6Y6g=,iv:0BnuZ9Uv2RgDwlisrVSvg7ESmNZvd8trggQDSJ42ewM=,tag:SXW2hbprj2qSRzjKY3Aw3Q==,type:str]
sops: sops:
age: age:
- recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p - recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
@@ -42,8 +44,8 @@ sops:
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g== wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-10T20:36:03Z" lastmodified: "2026-05-13T20:49:38Z"
mac: ENC[AES256_GCM,data:aEC9pHnupssTzcw9HdtqkzzhsNkkJMYT3qiwKPLCcIfDMN1Lv3Msi0TJyFjqjR/vzOfAyHFgsPjSWFladL7fOZHpqq2VeNYHPF9/GKEuoEMqsISN2FczqrTHNC8aI/vhZxe3BxgkX9neiHR9v31MRpX9lq6AbCrEJ42hCh6rCxs=,iv:41Dx92loo4zgKt+7iqjgaOZZUe58VAGEGgOEHEuztzQ=,tag:Oagh8hxWfXWuNI70WHULYA==,type:str] mac: ENC[AES256_GCM,data:2iQyp3U5KYCHIWvBsEyz8XFLTtQ5dN+2TF1gkFADFCyyJLAPWAxYPSH60d7fhJ5qhs7IJ7GV/N1J23JsXV+jyqS95foF9ThYT/wNeh4cAPGWB5RbnpP9RsYt8nCEIl/RHkkGmnS9HUO2HHpqo7hMUGRCHMLYMxJxHdPGrm+KHgA=,iv:D+x06308n14/xkRR9WvD6MYcORVM+crIH20+oHesHds=,tag:q7L7OyXEXThYFEkPrgzSBw==,type:str]
pgp: pgp:
- created_at: "2026-04-21T06:39:49Z" - created_at: "2026-04-21T06:39:49Z"
enc: |- enc: |-
+1
View File
@@ -2,6 +2,7 @@
pkgs.mkShell { pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
alejandra alejandra
sops
(pkgs.writeShellScriptBin "homey-deploy-rpi-main" '' (pkgs.writeShellScriptBin "homey-deploy-rpi-main" ''
nixos-rebuild switch \ nixos-rebuild switch \
--flake .#pi-main \ --flake .#pi-main \