Notifications sort of fixed
This commit is contained in:
+38
-10
@@ -134,17 +134,45 @@ Some services require manual one-time configuration after the first deploy.
|
|||||||
|
|
||||||
** Ntfy — push notifications
|
** Ntfy — push notifications
|
||||||
|
|
||||||
Ntfy's admin user is created automatically from sops on first start. You
|
Ntfy's admin user is created automatically from sops on first start.
|
||||||
still need to create a phone token and subscribe to the alerts topic.
|
|
||||||
|
|
||||||
1. Visit =https://ntfy.zakobar.com= and log in with the admin password
|
*** Step 1 — Generate VAPID keys (Web Push)
|
||||||
(=ntfy/admin_password= in =secrets/secrets.yaml=).
|
|
||||||
2. Go to *Account → Access Tokens → Create token* — give it a name (e.g.
|
Run on the Pi *before* the first full deploy:
|
||||||
"phone") and copy the token value.
|
|
||||||
3. In the [[https://ntfy.sh][Ntfy mobile app]]:
|
#+begin_src bash
|
||||||
- *Server*: =https://ntfy.zakobar.com=
|
ssh admin@192.168.1.100 'sudo ntfy webpush keys'
|
||||||
- *Access token*: the token you just created
|
#+end_src
|
||||||
4. Subscribe to the =alerts= topic in the app.
|
|
||||||
|
This prints a public key and a private key.
|
||||||
|
|
||||||
|
- Copy the *public key* into =hosts/pi-main/default.nix=:
|
||||||
|
#+begin_src nix
|
||||||
|
homey.ntfy.webPushPublicKey = "<public-key>";
|
||||||
|
homey.ntfy.webPushEmail = "mailto:you@zakobar.com";
|
||||||
|
#+end_src
|
||||||
|
- Add the *private key* to sops:
|
||||||
|
#+begin_src bash
|
||||||
|
sops secrets/secrets.yaml
|
||||||
|
# add: ntfy/web_push_private_key: <private-key>
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
The private key is injected at boot and never lands in the nix store.
|
||||||
|
|
||||||
|
*** Step 2 — Subscribe via Safari PWA (recommended for iOS)
|
||||||
|
|
||||||
|
1. Visit =https://ntfy.zakobar.com= in Safari and log in with the admin
|
||||||
|
password (=ntfy/admin_password= in =secrets/secrets.yaml=).
|
||||||
|
2. Go to *Account → Access Tokens → Create token* — give it a name and
|
||||||
|
copy the value.
|
||||||
|
3. Log in with the token, then tap *Share → Add to Home Screen*.
|
||||||
|
4. Open the app from the Home Screen (must be launched from there, not
|
||||||
|
Safari, to get push permission).
|
||||||
|
5. Subscribe to the =alerts= topic and grant notification permission when
|
||||||
|
prompted.
|
||||||
|
|
||||||
|
Web Push via the PWA uses Apple's APNs directly and is more reliable on
|
||||||
|
iOS than the native ntfy app's upstream relay.
|
||||||
|
|
||||||
** Uptime Kuma — notifications (two-deploy process)
|
** Uptime Kuma — notifications (two-deploy process)
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,10 @@
|
|||||||
# Monitoring stack
|
# Monitoring stack
|
||||||
homey.uptimeKuma.enable = true;
|
homey.uptimeKuma.enable = true;
|
||||||
homey.ntfy.enable = true;
|
homey.ntfy.enable = true;
|
||||||
|
# Generate with: ssh admin@192.168.1.100 'sudo ntfy webpush keys'
|
||||||
|
# Add private key to sops: ntfy/web_push_private_key
|
||||||
|
homey.ntfy.webPushPublicKey = "BE2qZVa3JEF741WTPtLevyhfP0I8bV0sD2a9-_y9NoyC40sgLpQi7bcoZesBwZEpRz8oiTVuoUFnHbckAsBQI5U";
|
||||||
|
homey.ntfy.webPushEmail = "aner@zakobar.com";
|
||||||
homey.monitoring.enable = true;
|
homey.monitoring.enable = true;
|
||||||
|
|
||||||
# Backups
|
# Backups
|
||||||
|
|||||||
+74
-33
@@ -11,11 +11,17 @@
|
|||||||
# - Caddy does NOT put forward_auth here; ntfy has native token/password auth
|
# - Caddy does NOT put forward_auth here; ntfy has native token/password auth
|
||||||
# so the mobile app can connect without Authelia SSO complications.
|
# 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:
|
# Setup after first deploy:
|
||||||
# 1. Visit https://ntfy.zakobar.com — log in with the admin password from sops.
|
# 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).
|
# 2. Create an access token for your phone (Admin → Users & Tokens).
|
||||||
# 3. In the Ntfy app: server = https://ntfy.zakobar.com, token = <your-token>.
|
# 3. PWA: open https://ntfy.zakobar.com in Safari → Share → Add to Home Screen,
|
||||||
# 4. Subscribe to the "alerts" topic.
|
# then open from Home Screen and subscribe to "alerts".
|
||||||
#
|
#
|
||||||
# Volume layout:
|
# Volume layout:
|
||||||
# <dataDir>/ntfy/auth.db ← user/token database
|
# <dataDir>/ntfy/auth.db ← user/token database
|
||||||
@@ -24,11 +30,35 @@
|
|||||||
#
|
#
|
||||||
# Secrets consumed from sops:
|
# Secrets consumed from sops:
|
||||||
# ntfy/admin_password
|
# ntfy/admin_password
|
||||||
|
# ntfy/web_push_private_key
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.homey.ntfy;
|
cfg = config.homey.ntfy;
|
||||||
dataDir = config.homey.storage.mountPoint;
|
dataDir = config.homey.storage.mountPoint;
|
||||||
domain = homeyConfig.domain;
|
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
|
in
|
||||||
{
|
{
|
||||||
options.homey.ntfy = {
|
options.homey.ntfy = {
|
||||||
@@ -39,40 +69,31 @@ in
|
|||||||
default = 2586;
|
default = 2586;
|
||||||
description = "Host port ntfy listens on (bound to 127.0.0.1).";
|
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 {
|
config = lib.mkIf cfg.enable {
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Secrets
|
# Secrets
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
sops.secrets."ntfy/admin_password" = { owner = "root"; };
|
sops.secrets."ntfy/admin_password" = { owner = "root"; };
|
||||||
|
sops.secrets."ntfy/web_push_private_key" = { owner = "root"; };
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# ntfy-sh native NixOS service
|
# ntfy-sh native NixOS service
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
services.ntfy-sh = {
|
services.ntfy-sh = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
settings = ntfySettings;
|
||||||
# Bind to localhost; Caddy reverse-proxies it
|
|
||||||
listen-http = "127.0.0.1:${toString cfg.port}";
|
|
||||||
base-url = "https://ntfy.${domain}";
|
|
||||||
|
|
||||||
# Require auth on all topics — deny unauthenticated access entirely
|
|
||||||
auth-default-access = "deny-all";
|
|
||||||
|
|
||||||
# Persistent state on external HD
|
|
||||||
auth-file = "${dataDir}/ntfy/auth.db";
|
|
||||||
cache-file = "${dataDir}/ntfy/cache.db";
|
|
||||||
attachment-root = "${dataDir}/ntfy/attachments";
|
|
||||||
|
|
||||||
# Keep messages for 12 hours so the app catches up if offline
|
|
||||||
cache-duration = "12h";
|
|
||||||
|
|
||||||
# Attachment limits
|
|
||||||
attachment-total-size-limit = "5G";
|
|
||||||
attachment-file-size-limit = "15M";
|
|
||||||
attachment-expiry-duration = "3h";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Minimal config for the `ntfy user` CLI — the NixOS module puts its
|
# Minimal config for the `ntfy user` CLI — the NixOS module puts its
|
||||||
@@ -107,12 +128,30 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
# Ensure ntfy-sh starts after the HD is mounted and dirs are ready.
|
# Ensure ntfy-sh starts after the HD is mounted and dirs are ready.
|
||||||
# Also widen ReadWritePaths so ntfy-sh can write to the external HD path
|
# Widen ReadWritePaths so ntfy-sh can write to the external HD.
|
||||||
# (the NixOS module restricts writes to /var/lib/ntfy-sh by default).
|
# 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 = {
|
systemd.services.ntfy-sh = {
|
||||||
after = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ];
|
after = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ];
|
||||||
requires = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ];
|
requires = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ];
|
||||||
serviceConfig.ReadWritePaths = lib.mkAfter [ "${dataDir}/ntfy" ];
|
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";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
@@ -145,13 +184,15 @@ in
|
|||||||
# Use the minimal CLI config (just has auth-file path).
|
# 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="${pkgs.ntfy-sh}/bin/ntfy user --config /etc/ntfy-sh/user-cli.yml"
|
||||||
|
|
||||||
# ntfy user list outputs a Unicode table; grep for admin in it.
|
|
||||||
# ntfy user add reads password + confirmation from stdin (two lines).
|
# ntfy user add reads password + confirmation from stdin (two lines).
|
||||||
if $NTFY list 2>/dev/null | grep -qE "admin"; then
|
# If the user already exists ntfy exits 1 with "already exists" — treat that as success.
|
||||||
echo "ntfy-sh-setup: admin user already exists"
|
if out=$(printf '%s\n%s\n' "$PASS" "$PASS" | $NTFY add --role=admin admin 2>&1); then
|
||||||
else
|
|
||||||
printf '%s\n%s\n' "$PASS" "$PASS" | $NTFY add --role=admin admin
|
|
||||||
echo "ntfy-sh-setup: admin user created"
|
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
|
fi
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ uptime-kuma:
|
|||||||
admin_password: ENC[AES256_GCM,data:tPKWxWmxRcVJeywY3J4eXAWWnAinLwMn3X68TrV/4emonvRiuyPmiwhn2fjDxwB/kT78y/iDDmpdQY229yJrkQ==,iv:YSL40PDbRTgtSYZCwqHzfJTcEAiILIDbGRA2kfamiw8=,tag:pMM0AWkjkcS9XOaSHG1oUQ==,type:str]
|
admin_password: ENC[AES256_GCM,data:tPKWxWmxRcVJeywY3J4eXAWWnAinLwMn3X68TrV/4emonvRiuyPmiwhn2fjDxwB/kT78y/iDDmpdQY229yJrkQ==,iv:YSL40PDbRTgtSYZCwqHzfJTcEAiILIDbGRA2kfamiw8=,tag:pMM0AWkjkcS9XOaSHG1oUQ==,type:str]
|
||||||
ntfy:
|
ntfy:
|
||||||
admin_password: ENC[AES256_GCM,data:P5pjnt00lyeGVlrBvUlJWWeTi3evFZPJIxjcsndbo4LZOLk6hbbrh8RwCAGzr1ump0A5fRXqynByRFdaS6++wA==,iv:Uxeh0/mygR++4S//O/RO2bouH2J0qcSCYtjjyZNooNk=,tag:LGIDaq4RzBuzrWFqVDr8ow==,type:str]
|
admin_password: ENC[AES256_GCM,data:P5pjnt00lyeGVlrBvUlJWWeTi3evFZPJIxjcsndbo4LZOLk6hbbrh8RwCAGzr1ump0A5fRXqynByRFdaS6++wA==,iv:Uxeh0/mygR++4S//O/RO2bouH2J0qcSCYtjjyZNooNk=,tag:LGIDaq4RzBuzrWFqVDr8ow==,type:str]
|
||||||
|
web_push_private_key: ENC[AES256_GCM,data:BggPo7uYjda48iV3G8TaPk7mPZXHv+H6MW3BeMYFaxYCVAok0zT7Tzko7A==,iv:qPX8N4mzD4DWX2tWlsQCK09PD0R4ntrJMqYOqwwzGXg=,tag:pXIp3pAkYQpdbXG/PtsFag==,type:str]
|
||||||
grafana:
|
grafana:
|
||||||
secret_key: ENC[AES256_GCM,data:/KNDMZZN5thoqsgJZS7fuNQULI1PAKVuihRu9WzO00Qw8js/V4KKJT0JOVOcqdHAnf44+szYZaCWt0xe02chGw==,iv:Y0FQ7h4SqZVtz0wLjPnVGGYyXmBIDi8nzaK2GFzDxqQ=,tag:w0z5/vI3Hfd8ry9DCHAvJw==,type:str]
|
secret_key: ENC[AES256_GCM,data:/KNDMZZN5thoqsgJZS7fuNQULI1PAKVuihRu9WzO00Qw8js/V4KKJT0JOVOcqdHAnf44+szYZaCWt0xe02chGw==,iv:Y0FQ7h4SqZVtz0wLjPnVGGYyXmBIDi8nzaK2GFzDxqQ=,tag:w0z5/vI3Hfd8ry9DCHAvJw==,type:str]
|
||||||
openldap:
|
openldap:
|
||||||
@@ -41,8 +42,8 @@ sops:
|
|||||||
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
|
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
|
||||||
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
|
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2026-05-10T08:30:12Z"
|
lastmodified: "2026-05-10T20:36:03Z"
|
||||||
mac: ENC[AES256_GCM,data:4nZyQLRlIWmgawj7YgL9DZAbbA/bQrOpfmFPlwaoPJR16eOQOHzxLLYrM6iyJxhYyt9d6iaFpFO9g9KOvOCDCG9s8/EImrXg0WDYIJn7D+ftGc0Qj5augBqeJEh9DgfDoWiXAYrCR3lUfDAswaSAPAjf1NzuStlM9X0SNyW+1Ug=,iv:mBq1xiTbjMX834yTnBR6+/IP8vZTYS8UB3v1z0wEc8s=,tag:BQWlnIj636Lv6a2CZjcV0w==,type:str]
|
mac: ENC[AES256_GCM,data:aEC9pHnupssTzcw9HdtqkzzhsNkkJMYT3qiwKPLCcIfDMN1Lv3Msi0TJyFjqjR/vzOfAyHFgsPjSWFladL7fOZHpqq2VeNYHPF9/GKEuoEMqsISN2FczqrTHNC8aI/vhZxe3BxgkX9neiHR9v31MRpX9lq6AbCrEJ42hCh6rCxs=,iv:41Dx92loo4zgKt+7iqjgaOZZUe58VAGEGgOEHEuztzQ=,tag:Oagh8hxWfXWuNI70WHULYA==,type:str]
|
||||||
pgp:
|
pgp:
|
||||||
- created_at: "2026-04-21T06:39:49Z"
|
- created_at: "2026-04-21T06:39:49Z"
|
||||||
enc: |-
|
enc: |-
|
||||||
|
|||||||
Reference in New Issue
Block a user