feat: add ntfy notification system
This commit is contained in:
parent
150a032988
commit
d8eb53f096
129
roles/ntfy/README.md
Normal file
129
roles/ntfy/README.md
Normal 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`)
|
||||||
32
roles/ntfy/defaults/main.yml
Normal file
32
roles/ntfy/defaults/main.yml
Normal 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
|
||||||
15
roles/ntfy/handlers/main.yml
Normal file
15
roles/ntfy/handlers/main.yml
Normal 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
4
roles/ntfy/meta/main.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
dependencies:
|
||||||
|
- role: podman
|
||||||
|
- role: nginx
|
||||||
106
roles/ntfy/tasks/main.yml
Normal file
106
roles/ntfy/tasks/main.yml
Normal 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
|
||||||
23
roles/ntfy/templates/docker-compose.yml.j2
Normal file
23
roles/ntfy/templates/docker-compose.yml.j2
Normal 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 }}
|
||||||
60
roles/ntfy/templates/nginx-vhost.conf.j2
Normal file
60
roles/ntfy/templates/nginx-vhost.conf.j2
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
roles/ntfy/templates/ntfy.service.j2
Normal file
16
roles/ntfy/templates/ntfy.service.j2
Normal 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
|
||||||
65
roles/ntfy/templates/server.yml.j2
Normal file
65
roles/ntfy/templates/server.yml.j2
Normal 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"
|
||||||
Loading…
Reference in New Issue
Block a user