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