feat: add ntfy notification system

This commit is contained in:
Clément Désiles 2025-12-15 23:09:47 +01:00
parent 150a032988
commit d8eb53f096
No known key found for this signature in database
9 changed files with 450 additions and 0 deletions

129
roles/ntfy/README.md Normal file
View File

@ -0,0 +1,129 @@
# ntfy - Simple Notification Service
Deploys [ntfy](https://ntfy.sh/) - a simple HTTP-based pub-sub notification service.
## Security Model
**Secure by default:**
- `auth-default-access: deny-all` - No anonymous access
- `enable-signup: false` - No public registration
- `enable-login: true` - Authentication required
- `enable-reservations: true` - Only authenticated users can reserve topics
All notifications require authentication to send or receive.
## Configuration
### Required Variables
Set in inventory or vault:
```yaml
ntfy_admin_password: "your-secure-password-here" # Min 12 chars
```
### Optional Variables
See [defaults/main.yml](defaults/main.yml) for all configuration options.
Key settings:
```yaml
ntfy_version: latest
ntfy_port: 8080
ntfy_base_url: http://localhost:8080
ntfy_admin_user: admin
# Nginx reverse proxy
ntfy_nginx_enabled: false
ntfy_nginx_hostname: ntfy.nas.local
```
## Usage
### Managing Users
List users:
```bash
podman exec ntfy ntfy user list
```
Add user:
```bash
podman exec ntfy ntfy user add <username>
```
Change password:
```bash
podman exec -i ntfy ntfy user change-pass <username>
```
Remove user:
```bash
podman exec ntfy ntfy user remove <username>
```
### Managing Topic Access
Grant access to topic:
```bash
podman exec ntfy ntfy access <username> <topic> <permission>
```
Permissions: `read-write`, `read-only`, `write-only`, `deny`
Example:
```bash
# Allow user to publish and subscribe to "alerts" topic
podman exec ntfy ntfy access alice alerts read-write
# Allow user to only publish to "monitoring" topic
podman exec ntfy ntfy access bob monitoring write-only
```
List access control:
```bash
podman exec ntfy ntfy access
```
### Publishing Notifications
Using curl with authentication:
```bash
curl -u admin:password -d "Backup completed" http://localhost:8080/backups
```
Using ntfy CLI:
```bash
ntfy publish --token <access-token> ntfy.nas.local mytopic "Hello World"
```
### Subscribing to Notifications
Web UI: https://ntfy.nas.local (if nginx enabled)
CLI:
```bash
ntfy subscribe --token <access-token> ntfy.nas.local mytopic
```
Mobile apps available for iOS and Android.
## Architecture
- **Container**: Podman-based deployment
- **Storage**: Persistent cache and user database
- **Networking**: Localhost binding by default
- **Reverse Proxy**: Optional nginx with HTTPS
## File Locations
- Configuration: `{{ podman_projects_dir }}/ntfy/server.yml`
- User database: `{{ ntfy_data_dir }}/user.db`
- Cache database: `{{ ntfy_cache_dir }}/cache.db`
- Attachments: `{{ ntfy_cache_dir }}/attachments/`
## Dependencies
- podman
- nginx (if `ntfy_nginx_enabled: true`)

View File

@ -0,0 +1,32 @@
---
# Ntfy version to deploy
ntfy_version: latest
# Storage location
ntfy_data_dir: "{{ podman_projects_dir }}/ntfy/data"
ntfy_cache_dir: "{{ podman_projects_dir }}/ntfy/cache"
# Authentication configuration (REQUIRED - must be set explicitly)
# Ntfy admin user for managing topics and access control
ntfy_admin_user: admin
# ntfy_admin_password: "" # Intentionally undefined - role will fail if not set
# Network configuration
ntfy_port: 8080
# Container image
ntfy_image: binwiederhier/ntfy
# Timezone
ntfy_timezone: UTC
# Server configuration
ntfy_base_url: http://localhost:{{ ntfy_port }}
ntfy_behind_proxy: false
ntfy_enable_signup: false # Disable public signup for security
ntfy_enable_login: true # Enable authentication
ntfy_enable_reservations: true # Only authenticated users can reserve topics
# Nginx reverse proxy configuration
ntfy_nginx_enabled: false
ntfy_nginx_hostname: ntfy.nas.local

View File

@ -0,0 +1,15 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart ntfy
ansible.builtin.systemd:
name: ntfy
state: restarted
daemon_reload: true
- name: Reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded

4
roles/ntfy/meta/main.yml Normal file
View File

@ -0,0 +1,4 @@
---
dependencies:
- role: podman
- role: nginx

106
roles/ntfy/tasks/main.yml Normal file
View File

@ -0,0 +1,106 @@
---
- name: Validate required passwords are set
ansible.builtin.assert:
that:
- ntfy_admin_password is defined
- ntfy_admin_password | length >= 12
fail_msg: |
ntfy_admin_password is required (min 12 chars).
See roles/ntfy/defaults/main.yml for configuration instructions.
success_msg: "Password validation passed"
- name: Create ntfy project directory
ansible.builtin.file:
path: "{{ podman_projects_dir }}/ntfy"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0755"
- name: Create ntfy data directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0755"
loop:
- "{{ ntfy_data_dir }}"
- "{{ ntfy_cache_dir }}"
- name: Deploy ntfy server configuration
ansible.builtin.template:
src: server.yml.j2
dest: "{{ podman_projects_dir }}/ntfy/server.yml"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0644"
notify: Restart ntfy
- name: Deploy docker-compose.yml for ntfy
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ podman_projects_dir }}/ntfy/docker-compose.yml"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0644"
notify: Restart ntfy
- name: Create systemd service for ntfy
ansible.builtin.template:
src: ntfy.service.j2
dest: /etc/systemd/system/ntfy.service
owner: root
group: root
mode: "0644"
notify: Reload systemd
- name: Enable and start ntfy service
ansible.builtin.systemd:
name: ntfy
enabled: true
state: started
daemon_reload: true
- name: Wait for ntfy to be ready
ansible.builtin.wait_for:
port: "{{ ntfy_port }}"
host: 127.0.0.1
timeout: 60
- name: Check if admin user already exists
ansible.builtin.command:
cmd: podman exec ntfy ntfy user list
register: ntfy_user_list
changed_when: false
failed_when: false
- name: Create admin user in ntfy
ansible.builtin.shell: |
printf '%s\n%s\n' '{{ ntfy_admin_password }}' '{{ ntfy_admin_password }}' | podman exec -i ntfy ntfy user add --role=admin {{ ntfy_admin_user }}
when: ntfy_admin_user not in ntfy_user_list.stdout
register: ntfy_user_create
changed_when: ntfy_user_create.rc == 0
- name: Set admin user password
ansible.builtin.shell: |
printf '%s\n%s\n' '{{ ntfy_admin_password }}' '{{ ntfy_admin_password }}' | podman exec -i ntfy ntfy user change-pass {{ ntfy_admin_user }}
when: ntfy_admin_user in ntfy_user_list.stdout
changed_when: false
- name: Deploy nginx vhost configuration for ntfy
ansible.builtin.template:
src: nginx-vhost.conf.j2
dest: /etc/nginx/conf.d/ntfy.conf
owner: root
group: root
mode: "0644"
when: ntfy_nginx_enabled
notify: Reload nginx
- name: Remove nginx vhost configuration for ntfy
ansible.builtin.file:
path: /etc/nginx/conf.d/ntfy.conf
state: absent
when: not ntfy_nginx_enabled
notify: Reload nginx

View File

@ -0,0 +1,23 @@
---
services:
ntfy:
container_name: ntfy
image: {{ ntfy_image }}:{{ ntfy_version }}
command:
- serve
volumes:
- /etc/localtime:/etc/localtime:ro
- {{ podman_projects_dir }}/ntfy/server.yml:/etc/ntfy/server.yml:ro
- {{ ntfy_cache_dir }}:/var/cache/ntfy:rw,Z
- {{ ntfy_data_dir }}:/var/lib/ntfy:rw,Z
ports:
- "{{ ntfy_port }}:80"
restart: always
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
environment:
TZ: {{ ntfy_timezone }}

View File

@ -0,0 +1,60 @@
# Ntfy vhost with Let's Encrypt (Certbot)
# Managed by Ansible - DO NOT EDIT MANUALLY
server {
listen 80;
server_name {{ ntfy_nginx_hostname }};
# Certbot webroot for ACME challenges
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl;
server_name {{ ntfy_nginx_hostname }};
# Let's Encrypt certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/{{ ntfy_nginx_hostname }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ ntfy_nginx_hostname }}/privkey.pem;
# SSL configuration
ssl_protocols {{ nginx_ssl_protocols }};
ssl_prefer_server_ciphers {{ 'on' if nginx_ssl_prefer_server_ciphers else 'off' }};
{% if nginx_log_backend == 'journald' %}
access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_ntfy;
error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_ntfy;
{% else %}
access_log /var/log/nginx/{{ ntfy_nginx_hostname }}_access.log main;
error_log /var/log/nginx/{{ ntfy_nginx_hostname }}_error.log;
{% endif %}
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:{{ ntfy_port }};
proxy_set_header Host $http_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;
# WebSocket and SSE support for ntfy
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Buffering must be off for SSE (Server-Sent Events)
proxy_buffering off;
# Timeouts for long-polling connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}

View File

@ -0,0 +1,16 @@
[Unit]
Description=Ntfy Notification Service
Requires=network-online.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory={{ podman_projects_dir }}/ntfy
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,65 @@
# Ntfy server configuration
# Managed by Ansible - DO NOT EDIT MANUALLY
# Public facing base URL of the service (e.g. https://ntfy.sh)
base-url: "{{ ntfy_base_url }}"
# Listen address for the HTTP & HTTPS web server. If "listen-https" is set, you must also
# set "key-file" and "cert-file". Format: [<ip>]:<port>, e.g. "1.2.3.4:8080".
listen-http: ":80"
# If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.
# This is optional and only required to support Android apps (which don't allow background
# tasks anymore). See https://ntfy.sh/docs/config/ for details.
# upstream-base-url: "https://ntfy.sh"
# Path to the private database file. If unset, the database is in memory.
cache-file: "/var/cache/ntfy/cache.db"
# Path to the attachment cache directory. Attachments are only stored if this is set.
attachment-cache-dir: "/var/cache/ntfy/attachments"
# If set, access tokens will be stored in this file. If unset, tokens are in-memory only.
auth-file: "/var/lib/ntfy/user.db"
# Default access level for new topics. Can be "read-write", "read-only", "write-only" or "deny-all".
# If "deny-all", no access is allowed by default and explicit ACLs must be configured.
auth-default-access: "deny-all"
# If enabled, allows users to sign up via the web app or API
enable-signup: {{ 'true' if ntfy_enable_signup else 'false' }}
# If enabled, allows users to log in via the web app or API
enable-login: {{ 'true' if ntfy_enable_login else 'false' }}
# If enabled, allows users to reserve topics via the web app or API (requires authentication)
enable-reservations: {{ 'true' if ntfy_enable_reservations else 'false' }}
# If set, the X-Forwarded-For header will be used to determine the visitor IP
behind-proxy: {{ 'true' if ntfy_behind_proxy else 'false' }}
# Interval in which keepalive messages are sent to the client. This is to prevent
# intermediaries from closing the connection for inactivity.
keepalive-interval: "45s"
# Interval in which the manager prunes old messages, deletes old attachments, and
# resets rate limiters. Note that these tasks are only executed if the interval has passed AND
# if there is traffic on the server.
manager-interval: "1m"
# Allowed origins for web app (CORS). Defaults to "*", which is fine for most cases.
# web-root: "/"
# Rate limiting: Number of requests allowed per visitor
visitor-request-limit-burst: 60
visitor-request-limit-replenish: "5s"
# Size limits
message-size-limit: "4096"
attachment-file-size-limit: "15M"
attachment-total-size-limit: "5G"
attachment-expiry-duration: "3h"
# Visitor limits
visitor-attachment-total-size-limit: "100M"
visitor-attachment-daily-bandwidth-limit: "500M"