211 lines
8.6 KiB
Nix
211 lines
8.6 KiB
Nix
{ config, lib, pkgs, homeyConfig, ... }:
|
|
|
|
# Ntfy — self-hosted push notification server.
|
|
#
|
|
# Mobile app (Android/iOS) connects to https://ntfy.zakobar.com with a token
|
|
# and subscribes to the "alerts" topic. Uptime Kuma and Grafana send alerts
|
|
# to that topic when services go down.
|
|
#
|
|
# Auth model:
|
|
# - Web UI: public-facing but ntfy enforces its own auth (deny-all by default)
|
|
# - Caddy does NOT put forward_auth here; ntfy has native token/password auth
|
|
# so the mobile app can connect without Authelia SSO complications.
|
|
#
|
|
# Web Push (PWA via Safari "Add to Home Screen"):
|
|
# Generate VAPID keys on the Pi:
|
|
# sudo ntfy webpush keys
|
|
# Set homey.ntfy.webPushPublicKey and homey.ntfy.webPushEmail in default.nix.
|
|
# Add the private key to sops: ntfy/web_push_private_key
|
|
#
|
|
# Setup after first deploy:
|
|
# 1. Visit https://ntfy.zakobar.com — log in with the admin password from sops.
|
|
# 2. Create an access token for your phone (Admin → Users & Tokens).
|
|
# 3. PWA: open https://ntfy.zakobar.com in Safari → Share → Add to Home Screen,
|
|
# then open from Home Screen and subscribe to "alerts".
|
|
#
|
|
# Volume layout:
|
|
# <dataDir>/ntfy/auth.db ← user/token database
|
|
# <dataDir>/ntfy/cache.db ← message cache (for missed messages)
|
|
# <dataDir>/ntfy/attachments/ ← file attachments
|
|
#
|
|
# Secrets consumed from sops:
|
|
# ntfy/admin_password
|
|
# ntfy/web_push_private_key
|
|
|
|
let
|
|
cfg = config.homey.ntfy;
|
|
dataDir = config.homey.storage.mountPoint;
|
|
domain = homeyConfig.domain;
|
|
|
|
# All ntfy settings in one place. The private key is NOT here — it is
|
|
# injected at runtime via ExecStartPre so it never lands in the nix store.
|
|
ntfySettings = {
|
|
listen-http = "127.0.0.1:${toString cfg.port}";
|
|
base-url = "https://ntfy.${domain}";
|
|
auth-default-access = "deny-all";
|
|
auth-file = "${dataDir}/ntfy/auth.db";
|
|
cache-file = "${dataDir}/ntfy/cache.db";
|
|
attachment-root = "${dataDir}/ntfy/attachments";
|
|
upstream-base-url = "https://ntfy.sh";
|
|
cache-duration = "12h";
|
|
attachment-total-size-limit = "5G";
|
|
attachment-file-size-limit = "15M";
|
|
attachment-expiry-duration = "3h";
|
|
web-push-public-key = cfg.webPushPublicKey;
|
|
web-push-email-address = cfg.webPushEmail;
|
|
web-push-file = "${dataDir}/ntfy/webpush.db";
|
|
};
|
|
|
|
# Build-time base config (no private key). ExecStartPre copies this to
|
|
# /run/ntfy-sh/server.yml and appends web-push-private-key from the credential.
|
|
baseConfigFile = (pkgs.formats.yaml {}).generate "ntfy-server-base.yml" ntfySettings;
|
|
in
|
|
{
|
|
options.homey.ntfy = {
|
|
enable = lib.mkEnableOption "Ntfy push notification server" // { default = true; };
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 2586;
|
|
description = "Host port ntfy listens on (bound to 127.0.0.1).";
|
|
};
|
|
|
|
webPushPublicKey = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "VAPID public key for Web Push (generate with: sudo ntfy webpush keys).";
|
|
};
|
|
|
|
webPushEmail = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Contact e-mail sent in VAPID headers (e.g. mailto:you@example.com).";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# -----------------------------------------------------------------------
|
|
# Secrets
|
|
# -----------------------------------------------------------------------
|
|
sops.secrets."ntfy/admin_password" = { owner = "root"; };
|
|
sops.secrets."ntfy/web_push_private_key" = { owner = "root"; };
|
|
|
|
# -----------------------------------------------------------------------
|
|
# ntfy-sh native NixOS service
|
|
# -----------------------------------------------------------------------
|
|
services.ntfy-sh = {
|
|
enable = true;
|
|
settings = ntfySettings;
|
|
};
|
|
|
|
# Minimal config for the `ntfy user` CLI — the NixOS module puts its
|
|
# generated config in the nix store under an unpredictable path, so we
|
|
# write a separate file just containing the auth-file path. The server
|
|
# ignores this file (it uses the module-generated one via -c flag).
|
|
environment.etc."ntfy-sh/user-cli.yml" = {
|
|
text = "auth-file: ${dataDir}/ntfy/auth.db\n";
|
|
mode = "0444";
|
|
};
|
|
|
|
# Ensure ntfy-sh starts after the HD is mounted and dirs are ready.
|
|
# Widen ReadWritePaths so ntfy-sh can write to the external HD.
|
|
# Inject the VAPID private key at runtime: ExecStartPre copies the
|
|
# build-time base config to /run/ntfy-sh/server.yml and appends the key,
|
|
# then we override ExecStart to use that runtime config file.
|
|
systemd.services.ntfy-sh = {
|
|
after = lib.mkAfter [ "mnt-data.mount" "systemd-tmpfiles-setup.service" ];
|
|
requires = lib.mkAfter [ "mnt-data.mount" ];
|
|
serviceConfig = {
|
|
ReadWritePaths = lib.mkAfter [ "${dataDir}/ntfy" ];
|
|
RuntimeDirectory = "ntfy-sh"; # creates /run/ntfy-sh, owned by ntfy-sh user
|
|
# Run as root (+) so the module's sandbox hardening can't block the write.
|
|
# Read the sops secret directly — no LoadCredential needed.
|
|
ExecStartPre = "+" + toString (pkgs.writeShellScript "ntfy-write-config" ''
|
|
set -euo pipefail
|
|
mkdir -p /run/ntfy-sh
|
|
cp ${baseConfigFile} /run/ntfy-sh/server.yml
|
|
printf 'web-push-private-key: %s\n' \
|
|
"$(cat ${config.sops.secrets."ntfy/web_push_private_key".path})" \
|
|
>> /run/ntfy-sh/server.yml
|
|
chown ntfy-sh:ntfy-sh /run/ntfy-sh/server.yml
|
|
chmod 600 /run/ntfy-sh/server.yml
|
|
'');
|
|
ExecStart = lib.mkForce "${pkgs.ntfy-sh}/bin/ntfy serve -c /run/ntfy-sh/server.yml";
|
|
};
|
|
};
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Create the admin user on first start (idempotent)
|
|
# -----------------------------------------------------------------------
|
|
systemd.services.ntfy-sh-setup = {
|
|
description = "Create Ntfy admin user";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "ntfy-sh.service" "mnt-data.mount" ];
|
|
requires = [ "ntfy-sh.service" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
LoadCredential = "ntfy_admin_password:${config.sops.secrets."ntfy/admin_password".path}";
|
|
|
|
ExecStart = pkgs.writeShellScript "ntfy-create-admin" ''
|
|
set -euo pipefail
|
|
|
|
# Wait until ntfy HTTP endpoint is ready (max 60 s)
|
|
for i in $(seq 1 30); do
|
|
if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/v1/health > /dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
PASS=$(cat "$CREDENTIALS_DIRECTORY/ntfy_admin_password")
|
|
|
|
# Use the minimal CLI config (just has auth-file path).
|
|
NTFY="${pkgs.ntfy-sh}/bin/ntfy user --config /etc/ntfy-sh/user-cli.yml"
|
|
|
|
# ntfy user add reads password + confirmation from stdin (two lines).
|
|
# If the user already exists ntfy exits 1 with "already exists" — treat that as success.
|
|
if out=$(printf '%s\n%s\n' "$PASS" "$PASS" | $NTFY add --role=admin admin 2>&1); then
|
|
echo "ntfy-sh-setup: admin user created"
|
|
elif echo "$out" | grep -q "already exists"; then
|
|
echo "ntfy-sh-setup: admin user already exists (ok)"
|
|
else
|
|
echo "$out" >&2
|
|
exit 1
|
|
fi
|
|
'';
|
|
};
|
|
};
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Caddy virtual host — no forward_auth; ntfy uses its own token auth
|
|
# -----------------------------------------------------------------------
|
|
homey.caddy.virtualHosts = [{
|
|
subdomain = "ntfy";
|
|
port = cfg.port;
|
|
auth = false;
|
|
}];
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Storage directories (owned by the ntfy-sh system user)
|
|
# -----------------------------------------------------------------------
|
|
homey.storage.extraDirs = [
|
|
{ path = "ntfy"; user = "ntfy-sh"; group = "ntfy-sh"; }
|
|
{ path = "ntfy/attachments"; user = "ntfy-sh"; group = "ntfy-sh"; }
|
|
];
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Backup
|
|
# -----------------------------------------------------------------------
|
|
homey.backup.extraPaths = [ "${dataDir}/ntfy" ];
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Uptime Kuma monitor for this service
|
|
# -----------------------------------------------------------------------
|
|
homey.monitoring.monitors = [{
|
|
name = "Ntfy";
|
|
url = "https://ntfy.${domain}/v1/health";
|
|
interval = 60;
|
|
}];
|
|
};
|
|
}
|