diff --git a/roles/ntfy/README.md b/roles/ntfy/README.md new file mode 100644 index 0000000..8fbb26e --- /dev/null +++ b/roles/ntfy/README.md @@ -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 +``` + +Change password: +```bash +podman exec -i ntfy ntfy user change-pass +``` + +Remove user: +```bash +podman exec ntfy ntfy user remove +``` + +### Managing Topic Access + +Grant access to topic: +```bash +podman exec ntfy ntfy access +``` + +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 ntfy.nas.local mytopic "Hello World" +``` + +### Subscribing to Notifications + +Web UI: https://ntfy.nas.local (if nginx enabled) + +CLI: +```bash +ntfy subscribe --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`) diff --git a/roles/ntfy/defaults/main.yml b/roles/ntfy/defaults/main.yml new file mode 100644 index 0000000..bca715e --- /dev/null +++ b/roles/ntfy/defaults/main.yml @@ -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 diff --git a/roles/ntfy/handlers/main.yml b/roles/ntfy/handlers/main.yml new file mode 100644 index 0000000..5889aeb --- /dev/null +++ b/roles/ntfy/handlers/main.yml @@ -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 diff --git a/roles/ntfy/meta/main.yml b/roles/ntfy/meta/main.yml new file mode 100644 index 0000000..89d056b --- /dev/null +++ b/roles/ntfy/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: podman + - role: nginx diff --git a/roles/ntfy/tasks/main.yml b/roles/ntfy/tasks/main.yml new file mode 100644 index 0000000..9675cd5 --- /dev/null +++ b/roles/ntfy/tasks/main.yml @@ -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 diff --git a/roles/ntfy/templates/docker-compose.yml.j2 b/roles/ntfy/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..75e37fa --- /dev/null +++ b/roles/ntfy/templates/docker-compose.yml.j2 @@ -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 }} diff --git a/roles/ntfy/templates/nginx-vhost.conf.j2 b/roles/ntfy/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..5e09b6c --- /dev/null +++ b/roles/ntfy/templates/nginx-vhost.conf.j2 @@ -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; + } +} diff --git a/roles/ntfy/templates/ntfy.service.j2 b/roles/ntfy/templates/ntfy.service.j2 new file mode 100644 index 0000000..13477c5 --- /dev/null +++ b/roles/ntfy/templates/ntfy.service.j2 @@ -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 diff --git a/roles/ntfy/templates/server.yml.j2 b/roles/ntfy/templates/server.yml.j2 new file mode 100644 index 0000000..58de79a --- /dev/null +++ b/roles/ntfy/templates/server.yml.j2 @@ -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: []:, 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"