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
- func ComposeYAML(serverYAMLPath string) []byte
- func EnsureRunning(ctx context.Context, runner *dockerexec.Runner, dataDir string, p Params, ...) error
- func GeneratePassword() (string, error)
- func GenerateToken() (string, error)
- func Materialize(dataDir string, p Params) (composePath string, err error)
- func ServerYAML(p Params) ([]byte, error)
- func Stop(ctx context.Context, runner *dockerexec.Runner, dataDir string, ...) error
- func Up(ctx context.Context, runner *dockerexec.Runner, dataDir string, ...) error
- type Params
Constants ¶
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" )
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).