ntfy

package
v0.4.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package ntfy lets Mooring MANAGE a self-hosted ntfy server so an operator can get push alerts without depending on the public ntfy.sh or hand-running a container. Like internal/socketproxy, the compose + config are Mooring-OWNED (generated, never operator input) and brought up via the dockerexec runner.

Security model (operator's choices): the server is LOCKED DOWN — auth-default-access is deny-all and access is granted only via two seeded tokens:

  • a WRITE-ONLY token Mooring uses to publish alerts (never shown to the operator),
  • a READ-ONLY token the operator puts in their phone's ntfy app to subscribe.

So a leaked phone token can only RECEIVE, never publish. TLS + the public hostname are handled by Mooring's managed edge (Caddy); iOS instant push uses the free ntfy.sh upstream relay (which only ever sees an opaque topic hash, never the name or body).

Index

Constants

View Source
const (
	// Project is the fixed compose project name for the managed ntfy (protected infra).
	Project = "mooring-ntfy"
	// Service is the compose service name (the edge routes hostname -> Service:Port).
	Service = "ntfy"
	// ContainerPort is ntfy's in-container HTTP port (Caddy reverse-proxies to it).
	ContainerPort = 80
	// LoopbackPort is the host-loopback port the container publishes to; Mooring
	// publishes alerts to http://127.0.0.1:LoopbackPort (never reachable off-host).
	LoopbackPort = 2586

	// Image is the ntfy server image, DIGEST-PINNED (plan §15 supply-chain posture, like
	// internal/socketproxy). It must be >= v2.14.0 — the first release with declarative
	// auth-users/auth-access/auth-tokens in server.yml, which is how this package seeds
	// the publisher/subscriber tokens (older images silently ignore those keys, so
	// deny-all would reject everything and no alert would ever arrive). This is the
	// multi-arch manifest-list digest for v2.24.0 (amd64/arm64/arm), resolved from the
	// registry. To bump: re-resolve with `docker buildx imagetools inspect binwiederhier/ntfy:<ver>`.
	Image = "binwiederhier/ntfy:v2.24.0@sha256:f8a9b104313b87cc24ae4f775f39e6328205b57dff6ede3eaf098a91e5d79f59"
)
View Source
const SubscriberUser = "phone"

SubscriberUser is the read-only account the operator signs into the ntfy app/web UI with to subscribe. The ntfy app + web UI authenticate with username+password (not a raw token), so the subscriber gets a real password — not a token they can't enter.

Variables

This section is empty.

Functions

func ComposeYAML

func ComposeYAML(serverYAMLPath string) []byte

ComposeYAML renders the compose for the managed ntfy. server.yml is bind-mounted read-only (0644 so the container user can read it; its parent dir stays 0700); state lives in named volumes (docker-owned perms). The HTTP port is published ONLY on 127.0.0.1 — Mooring publishes there; the public path is Caddy -> the bridge IP.

func EnsureRunning

func EnsureRunning(ctx context.Context, runner *dockerexec.Runner, dataDir string, p Params, onLine func(string)) error

EnsureRunning RE-materializes the config (fresh subscriber password + write token) and brings the managed ntfy up. UNGATED (infra plane) + best-effort, mirroring socketproxy: a docker error is returned to log, never fatal. Nothing operator-controlled reaches the docker argv (the compose is Mooring-generated; the bind path is Mooring-owned).

--force-recreate is REQUIRED here (this is the (re)provision path): server.yml is bind-mounted, and ntfy only provisions/reconciles its auth-users + ACL into user.db at PROCESS START. Plain `docker compose up -d` recreates a container only when the compose SPEC changes — NOT when a bind-mounted file's CONTENTS change — so without it the already-running ntfy keeps serving the OLD user.db (old subscriber password) while the dashboard shows the NEW one, and signing in fails with "user phone not authorized". Forcing a recreate restarts ntfy so it re-reads the rewritten server.yml and updates the provisioned user/ACL (ntfy provisioning is create-OR-update on restart). The named volumes (user.db, cache) persist across the recreate, so history is kept. This is the only re-materialize path; the boot-time Up() below must NOT force-recreate (it would churn the running container on every restart for no config change).

func GeneratePassword

func GeneratePassword() (string, error)

GeneratePassword returns a strong, app-typeable subscriber password (24 chars of [A-Za-z0-9], crypto/rand with rejection sampling — no modulo bias).

func GenerateToken

func GenerateToken() (string, error)

GenerateToken returns a fresh ntfy-format access token: "tk_" + 29 [a-z0-9] = 32 chars total, which is exactly what ntfy requires. Uses crypto/rand with rejection sampling so the alphabet is uniform (no modulo bias).

func Materialize

func Materialize(dataDir string, p Params) (composePath string, err error)

Materialize writes server.yml (0644, container-readable) and docker-compose.yml (0600) under dataDir/ntfy (dir 0700) and returns the compose path. Pure I/O (no docker), so it is unit-testable.

func ServerYAML

func ServerYAML(p Params) ([]byte, error)

ServerYAML renders the ntfy server.yml: locked down (deny-all) with two seeded users scoped to the topic. The publisher (mooring, write-only) authenticates with a token (Bearer, used over loopback). The subscriber (phone, read-only) authenticates with a real PASSWORD — because the ntfy app + web UI log in with username+password, not a raw token. Behind-proxy + base-url are set for Caddy; upstream-base-url enables iOS push via the ntfy.sh relay.

func Stop

func Stop(ctx context.Context, runner *dockerexec.Runner, dataDir string, onLine func(string)) error

Stop tears the managed ntfy down (`docker compose down`), keeping named volumes so a re-enable preserves message history. Best-effort.

func Up

func Up(ctx context.Context, runner *dockerexec.Runner, dataDir string, onLine func(string)) error

Up brings the EXISTING on-disk managed-ntfy compose up WITHOUT re-materializing the config (so it needs no tokens). Used at boot to reconcile the protected container to running if it was removed — restart:unless-stopped covers reboots, but not a manual `docker rm`. No-op error if it was never provisioned. Best-effort.

Types

type Params

type Params struct {
	BaseURL     string // the public https URL, e.g. "https://ntfy.example.com"
	Topic       string // the alert topic
	WriteToken  string // Mooring publisher token (wo on Topic)
	SubPassword string // the subscriber (phone) account password (ro on Topic)
}

Params is everything needed to render the server config for one managed instance. They're persisted by the caller (encrypted channel config) and passed back on every (re)materialize so the same credentials survive restarts. Mooring PUBLISHES with the write token (Bearer, over loopback); the operator SUBSCRIBES as SubscriberUser with SubPassword (read-only).

func (Params) Validate

func (p Params) Validate() error

Validate checks the params are well-formed before they drive config generation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL