Setting up private and secure NTFY messaging for HA notifications

When looking for information on how to use NTFY with Home Assistant I was unable to find anything, esp around the security element of an open (by default) messaging tool.
But I figured out the basics and a little more and thought I’d share a step-by-step method here to help bump start someone else’s journey that may be looking to do the same.

How it works:
NTFY follows the same idea as MQ(TT) where there are “topics” created and users subscribe to that topic to receive and/or send messages.
When content is added to a topic then all subscribers/devices of that topic receive a copy locally.
Messages are cached for a time and delivered as soon as connected.

Plan:
Set up two channels of notifications. One for general info and one for important info
Maybe one extra for family chitchat.

Pre-requisites

  1. A method of self hosting NTFY
  2. A method of sharing it externally using a domain

1 In our setup, the self-hosting method is Docker on a *nix system.
2 We use Nginx Proxy Manager (NPM) in docker for external access.
2b Alternatively you could use a Cloudflare Tunnel to do this.

.

Step 1: Prep
This is a typical directory structure for docker:
~/scripts/ - to hold the scripted docker launcher.
/opt/docker/[application] - docker mounted volumes for persistent data.

To create the folders you need to host NTFY, SSH into your host and:

mkdir /opt/docker/ntfy/cache

.

Step 2: Install
Ceate your Docker CLI script to create the container + add it to a shared docker network (v. useful if you’ll be using a Cloudflared tunnel). In this instance, “localproxy” has an IP range of 172.29.0.*
File: ~/scripts/ntfy

docker stop ntfy
docker rm ntfy
sleep 1
docker pull binwiederhier/ntfy

echo Starting ntfy
docker run -d \
--name ntfy \
-h ntfy \
--network=localproxy \
--ip 172.29.0.50 \
-p 8000:80 \
-e PUID=1000\
-e PGID=100 \
-e TZ=Europe/London \
-v /opt/docker/ntfy:/etc/ntfy \
-v /opt/docker/ntfy/cache:/var/cache/ntfy \
--restart=unless-stopped \
binwiederhier/ntfy \
serve

To make the script executeable:

chmod 755 ~/scripts/ntfy

( or to get a Docker-Compose equivalent, run the above CLI through https://www.composerize.com/)

Once it’s running you should be able to see it with the command:

docker stats ntfy

Now it’s running, you should be able to connect to your instance’ UI using a web browser: http://[DockerHost]:8000

.

Step 3: Setup External access
To access it externally, either add it to a ProxyManager or CloudFlare Tunnel.
Our NPM uses the same shared docker network “localproxy” so it can use appname:80 in the destination as seen below.
Otherwise we’d have to configure the destination using the defined docker network IP 172.29.0.50:8000


If configuring with Cloudflared tunnels, specifying the 172.29.0.50 IP in the docker CLI as we did should make it easier to configure.

.

Step 4: Lock it down
Right now it’s up and running, but there’s no users and therefore no security.
Anyone can connect and create a “topic” and start sending messages.

To lock it down we set to create the parameter file to tell NTFY what to do.
For some reason the default config is not copied to the server by default. I don’t know why.
You’ll see the docker folders are empty, so create file: /opt/docker/ntfy/server.yml
Either paste the default config in there or only use the bits you need.
This is how ours is set:

# You need this to be your external domain to receive notifications while afh.
base-url: "https://ourntfy.ourdomain.com"
# If you use IOS then you need to include the following. Not needed for Android + PC
upstream-base-url: "https://ntfy.sh"
# This section creates the cache database and defines the length of time to hold messages
cache-file: /var/cache/ntfy/cache.db
cache-duration: "24h"
cache-startup-queries:
   pragma journal_mode = WAL;
   pragma synchronous = normal;
   pragma temp_store = memory;
   pragma busy_timeout = 15000;
   vacuum;
# This creates a secondary database used to manage your users and permissions
auth-file: /var/cache/ntfy/auth.db
# auth-default-access: "read-write"
auth-default-access: "deny-all"
auth-startup-queries:
   pragma journal_mode = WAL;
   pragma synchronous = normal;
   pragma temp_store = memory;
   pragma busy_timeout = 15000;
   vacuum;
# I assume you also need this for CloudFlared Tunnel (not sure), not just a ProxyManager.
behind-proxy: true
# Useful if you plan to send CCTV snapshots 
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
# Default if 15000. Reset to a safer limit for our restricted use.
global-topic-limit: 50
# Rate limiting in case a message source goes a bit nuts.
visitor-subscription-limit: 10
visitor-attachment-total-size-limit: "50M"
visitor-attachment-daily-bandwidth-limit: "100M"

The eagle eyed may have noticed this important entry in the system.yml

auth-default-access: “deny-all”

This prevents people without a uname/passwd from being able to use your NTFY should they stumble across the external URL.
Nice.

.

Step 5: Creating users and Topics

Now it’s restricted we need to create some users and specific access they need.
This bit is all in CLI within the container. Here we go…
To get to the command line within your NTFY container, SSH to your docker host and use the following command

docker exec -it ntfy sh

Now your’e in, these are a list of useful help commands.

clear ; ntfy --help
clear ; ntfy user --help
clear ; ntfy access --help

What we’ll do now is create 4 example users:
1 Admin so you can create and subscribe to any topic
1 Home Assistant user to send messages
1 “General” user to be able to receive “general” Home Assistant messages
1 “important” user to receive hi-pri messages
(Use a strong password for the HomeAssistant User)

ntfy user add --role=admin phil
ntfy user add homeassistantnotify
ntfy user add kids
ntfy user add adults

Now the users are created (and passwords set)
By default noone has rites to do anything, so we need to set HA to be able to send only (wo) and the users to receive only (ro)

As we do this, we’ll also be defining what Topics we’ll be using

  1. HomeAssistantGen
  2. HomeAssistantSOS
  3. FamilyChat
ntfy access homeassistantnotify HomeAssistantGen wo
ntfy access homeassistantnotify HomeAssistantSOS wo
ntfy access kids HomeAssistantGen ro
ntfy access kids FamilyChat rw
ntfy access adults HomeAssistantGen ro
ntfy access adults HomeAssistantSOS ro
ntfy access adults FamilyChat rw

.

Step 6: Mobile setup
The next step is to configure your mobile device(s) to receive notifications.
Install the NTFY app (Google or Apple)

First thing to do is go to the 3 dot “kebab” menu on the top right and goto Settings.
In General, Select Default Server and use your https://ourntfy.ourdomain.com.
Note: Each subscribed topic lets you override the defaults if needed.

Now hit the (+) and subscribe to your topic. No need to “use another server” as you already set your personal one as the default. Saves you having to type it in every time.
When you enter HomeAsssitantGen/SOS or FamilyChat you’ll be prompted (once) to enter your username. If you have the rites to subscribe to that topic then it’ll appear in the app.
If you can’t and it looks like it isn’t responding then check for typos or double check your permissions.
Note: Each device will have one user it can connect as.

Now your mobile device is set up, add the same topics in the web browser (as admin) and post a message in one of the topics. It should appear on a mobile device that has that topic subscribed.
If the mobile user is Read Only (ro) then they shouldn’t be able to post a message back.
This will keep the topic clear only for real alerts from Home Assistant.

.

Step 7: Home Assistant Config
After all that, this is the easy bit.
There’s no NTFY integration in Home assistant. What it uses is the Apprise integration as middleware to take the message, transform it to the right format and send it on to a recipient. This could be any of a plethora of destinations… one of which is NTFY

To start, lets set our secure data in secrets.yaml
If your Home Assistant is in the same LAN as the NTFY server, then use the NTFY local IP.

  • ntfy://{user}:{password}@{host}:{port}/{topics}
# NTFY example LAN
ntfy_url_1: ntfy://homeassistantnotify:[email protected]:8008/HomeAssistantGen
ntfy_url_2: ntfy://homeassistantnotify:[email protected]:8008/HomeAssistantSOS

For an external Home Assistant connecting using your full site URL (internet needed). Note it’s now ntfyS as it has an SSL certificate + no longer needs the port :8000 defined

  • ntfys://{user}:{password}@{host}/{topics}
# NTFY example WAN
ntfy_url_1: ntfys://homeassistantnotify:[email protected]/HomeAssistantGen
ntfy_url_2: ntfys://homeassistantnotify:[email protected]/HomeAssistantSOS

Now in the configuration.yaml

notify:
  - name: ntfy_general_message
    platform: apprise
    url: !secret ntfy_url_1
  - name: ntfy_sos_message
    platform: apprise
    url: !secret ntfy_url_2

.

Step 8: Test
Now create an automation or script and test the notification.
Something like this (run manually)
Script:

alias: Notification Test
sequence:
  - service: notify.ntfy_general_message
    data:
      title: Test Message from Home Assistant
      message: oooo the zigbeee
mode: single
icon: mdi:bullhorn

You can add priority levels for the HA notifications as well as icons and attach images, but the above is the basics just to get NTFY installed and running.

But that’s it, it’s in, it’s secure and most of all, it’s fast.

For the visual learners out there and to see how to do the cloudflared method for external access is setup (as well as some other NTFY use cases), check out NetWorkChuck’s video on NTFY.
None of the security elements are mentioned or creating the cache/user databases, but it’s still useful. :slightly_smiling_face:

5 Likes

Addendum.
To set the priority it can be added using the Rest platform.
Perhaps it can with apprise, but I’ve not seen where to configure that yet.

Example of secrets.yaml

# NTFY credentials
ntfy_username: homeassistantnotify
ntfy_password: myfancypasswd

Example of configuration.yaml setting the message priority depending on the topic

notify:
  - name: ntfy_lowpri
    platform: rest
    method: POST_JSON
    authentication: basic
    username: !secret ntfy_uname
    password: !secret ntfy_passwd
    data:
      topic: HomeAssistantGen
      priority: 1
    title_param_name: title
    message_param_name: message
    resource: http://123.45.67.89:8008
  - name: ntfy_general_message
    platform: rest
    method: POST_JSON
    authentication: basic
    username: !secret ntfy_uname
    password: !secret ntfy_passwd
    data:
      topic: HomeAssistantGen
      priority: 3
    title_param_name: title
    message_param_name: message
    resource: http://123.45.67.89:8008
  - name: ntfy_sos_message
    platform: rest
    method: POST_JSON
    authentication: basic
    username: !secret ntfy_uname
    password: !secret ntfy_passwd
    data:
      topic: HomeAssistantSOS
      priority: 5
    title_param_name: title
    message_param_name: message
    resource: http://123.45.67.89:8008

By default the NTFY app will silently receive messages that are < priority 3
This means the notification.ntfy_lowpri still comes to the HomeAssistantGen topic on your device, but without any haptic or alert notification.

2 Likes

Nice. Are the subscribers per user or per device? I’ve been looking for something so that I can send notifications to my tablet and phone, but dismiss on the other device when I have already seen it.

This is my understanding of how it works.

The NTFY server is a queuing system that holds messages for a configured time
system.yml cache-duration: "24h" in post #1.

Every device that subscribes to the topic gets it’s own copy of the subscribed message as long as it connects before the the message expiry time has purged the message from the topic.
Once the client has it, even if the server expunges it locally, the client keeps it’s copy until you manually remove it.

This way all devices get a copy even when some are disconnected for a time. This mean that deleting from a single client won’t purge the message from the source or any other that will or have already received the message.

Maybe there is a way to remove it from the server (not other clients), but it’s not something I’ve looked into as it’s not how we use NTFY here.

afaik the only notification protocol that does remove-all is the one built into HA itself where you can issue a persistent_notification.create with a unique notification_id on an event and then a subsequent persistent_notification.dismiss notification_id to remove it.

e.g. We get an HA dashboard notification when our washing machine finishes. Another automation runs when the door is closed>open to auto-dismiss the dashboard message so we don’t have to clear it manually from all devices.

NTFY sends a message instantly and is suited for more immediate types of notifications like freezer temps too high or something you want everyone to see sooner rather than later.

Maybe leave the more low-pri daily tasks to the (slow) HA persistent notification which can be dismissed centrally.

1 Like

Thanks for the detailed explanation. I suppose you are right, I just usually get lazy and do not write the “dismiss” portion of the HA notifications.

I’m thinking of my Frigate notifications, e.g., a package was delivered. You are right, on HA I usually get them once the delivery guy is gone. And then, again later, which I pick up my phone and see it was the package I already knew about and went out to get. :slight_smile: