Repository navigation
Releases: ManasvinYadav/lantern
Release list
v0.72.0 — Tokens, sessions, 2FA, dependencies and long-range history
Upgrade: nothing to do. Every feature is opt-in and every fix is backward compatible.
Two behaviour changes worth knowing: POST /api/config/import now returns 400 and writes
nothing for a file it can't apply in full, instead of a 200 with a skipped list; and an
account with 2FA enabled can no longer sign in over HTTP Basic Auth, so use a scoped API
token for scripts.
Five features and six fixes. Most of the features are things the server could already do
that nothing in the UI could reach. The v0.71.0 audit kept walking into them.
Added
-
Scoped API tokens you can actually create. Per-service tokens have existed since
Phase 2, but the only way to mint one was to open the SQLite file and insert a row by
hand, hashing the value yourself first. Nobody does that, so jobs that only ever speak
for one service ended up carrying the installation-wideLANTERN_AUTH_TOKENinstead.Settings → Access now lists, creates and revokes them. The token is shown once, at
creation; only its hash is stored. Each row records when it was last used, so a token
nothing has touched in a year is visible as such.GET/POST /api/admin/tokens,DELETE /api/admin/tokens/{id}. They live under
/api/admin/, so a scoped token can't mint another one and a viewer can't see them. -
Active sessions. Every browser signed in as you: user agent, address, when it
started, when it was last seen. End any of them, or all the others at once. Available to
every role including viewers, since these routes only ever touch the caller's own rows.GET /api/auth/sessions,DELETE /api/auth/sessions[/{id}]. -
Two-factor auth (TOTP) per account, from Settings → Access. Enrolment is two steps:
the secret is issued first, and only a working code switches it on, so a mistyped setup
key fails now rather than at your next sign-in. Turning it back off needs the password
and a current code.HTTP Basic Auth is refused for an enrolled account. There is nowhere to put a six-digit
code in a Basic Auth header, so accepting it would be a documented bypass. Use a scoped
API token for scripts.Lost your authenticator?
LANTERN_DISABLE_TOTP=1on the process bypasses it for every
account. There are no recovery codes; whoever runs Lantern already has the host.GET/POST/PUT/DELETE /api/auth/totp. -
Service dependencies. When the router goes down everything behind it goes down, and
Lantern used to say so fifteen times. Name a service's parent and its outage alert is
suppressed while that parent is already down. The child still records the outage, still
shows red, and still counts it against its own uptime. This changes who gets told, not
what gets recorded.Suppression walks the whole ancestor chain. Router down, host down, container on that
host still suppressed. Cycles are refused when you save the edge.GET /api/dependencies,GET/PUT /api/services/{name}/dependency. -
90-day and 1-year uptime. Retention used to be a straight delete: past
LANTERN_RETENTION_DAYSa service's history was gone, and with it any way to answer what
last quarter looked like. Keeping the raw rows isn't an option either. A service checked
every 10 seconds writes 260k rows a month.So each hour is folded into one row before the delete: counts by outcome plus an average
latency, about 1/360th of the volume. The raw events still disappear on schedule. What
survives is their shape, for two years. The service drawer gains 90d and 1y
ranges that read from it.GET /api/services/{name}/rollups?days=N.
Fixed
-
Config import wasn't transactional, and said "ok" either way. It wrote as it went and
listed whatever failed underskipped, next to"status": "ok"and a 200. A file with
one bad monitor type left the instance half configured and called it a success, and you
had to diff an export against reality to find out what actually landed.Import is now all-or-nothing in two stages. The whole payload is validated before
anything is written, so a bad file is a 400 that changes nothing, and the writes run in
one transaction. A 200 from here now means the file applied in full. -
One webhook queue served all four channels, with no retry. A Discord endpoint that
had gone away filled all 256 slots with jobs waiting on a 10 second timeout, and Telegram
and Gotify alerts got dropped behind it. Those endpoints were fine.Each channel now has its own queue and its own worker. Transport errors, 5xx and 429 are
retried twice with a backoff. Any other 4xx is a revoked webhook rather than a blip, so
it isn't retried. Still onewebhook_deliveriesrow per job, recording the final
outcome. -
The monitor pool starved. Four fixed workers against a 10 second check timeout is a
ceiling of 24 checks a minute, however many services you have configured. Twelve monitors
on a 30 second interval with three of them timing out is already past it. Healthy checks
queued behind timeouts they had nothing to do with, ran late, reported stale, and were
eventually dropped, which reads on the dashboard as the services flapping rather than as
Lantern being oversubscribed.A saturated pool now grows a worker for the check that saturated it, up to 64.
-
X-Forwarded-Forwas ignored, so everything behind a reverse proxy shared one throttle
bucket. Keying the login throttle onRemoteAddris right when Lantern is exposed
directly and wrong behind Caddy or Traefik, where the proxy is the peer on every request.
One person's failed logins locked out the whole installation, and every audit row
recorded the same useless address. v0.71.0 made this sharper by throttling Basic Auth
too.New
LANTERN_TRUSTED_PROXIEStakes comma-separated CIDRs or bare IPs, empty by default.
When the immediate peer is one of them the forwarded chain is walked from the right,
discarding hops that are themselves trusted proxies. The right end is the one the proxy
appended, and the only end an outside caller can't forge; anything further left came from
the client and isn't evidence. Leave it empty and behaviour is exactly as before. -
An ambiguous container name silently picked one.
findDockerContainerfalls back to
the Compose service label and then to a name prefix or suffix match, and whichever
container the daemon happened to list first won. Two Compose projects on one host
routinely both have a service calleddb, andappsuffix-matches bothweb-appand
worker-app. The caller of this isPOST /api/services/{name}/docker/restart.An ambiguous tier is now an error naming the candidates, not a coin flip. Exact
container-name matches are unaffected, since those are unique per daemon. -
Service names were unbounded and unvalidated.
POST /api/statuscreates a service by
naming it, and nothing checked the name. A slash made/api/services/{name}/…
unroutable, so the service could never afterwards be edited or deleted. A control
character corrupted the Prometheus export and the SVG badge. An unbounded name meant an
unbounded row in every service-scoped table. Names are now capped at 128 characters and
rejected if they contain a slash or a control character.
Changed
- Deleting a service also clears its dependency edge, the edges pointing at it, and its
rollups. Deleting a user clears their 2FA enrolment; renaming one carries it across. - The retention janitor skips its delete if the rollup pass fails, so raw history is never
deleted without a folded copy of it.
Docker image: ghcr.io/manasvinyadav/lantern:v0.72.0
Full changelog: https://github.com/ManasvinYadav/lantern/blob/master/CHANGELOG.md
v0.70.1 — Startup log said auth was off while it was on
Fixed
- The
listening on :7654 (auth=...)line reportedLANTERN_AUTH_USER, not the
gateauthMiddlewareactually enforces. An install whose operator signs in
with a stored account — every install upgraded to v0.70.0 — logged
auth=falseon boot while refusing unauthenticated requests correctly. The
gate was never wrong; only the line describing it was. Pre-existing, but
v0.70.0 is where it started applying to the common case.
v0.70.0 — Multi-user accounts with roles
Closes the last item from the production-readiness audit. Lantern held exactly
one operator; it now has accounts.
Added
-
Accounts with three roles, at Settings → Users (owner only) or
/api/admin/users:viewer admin owner Read the dashboard, history, incidents ✅ ✅ ✅ Push status, trigger checks, maintenance mode ✅ ✅ Services, monitors, groups, alert routes, branding, quiet hours ✅ ✅ Backup, webhook URLs, config export/import, audit log ✅ ✅ Create, disable, re-role and remove accounts ✅ Roles are coarse deliberately. Per-service access for a person is not
something Lantern needs; per-service access for a machine already exists as
scoped API tokens, which are an orthogonal axis and are unchanged.A viewer is read-only and is additionally refused on the routes that carry
installation-wide secrets —GET /api/backupwould hand them every password
hash,GET /api/webhooksevery webhook URL. The gate runs in the auth
middleware before any handler; the dashboard hides controls a role cannot use,
but that is a courtesy, not the boundary.Roles are read from the database on every request rather than stamped onto the
session, so demoting or disabling someone binds on their very next request
rather than at their next sign-in. -
Guardrails, each refused with
409: the last enabled owner cannot be
deleted, disabled or demoted, and you cannot delete the account you are signed
in as. Deleting or disabling an account, or resetting its password, ends that
account's sessions immediately. -
GET /api/auth/sessionnow reportsrole, so the dashboard can hide what
a caller may not do. -
docs/CUSTOM_DOMAIN.md and a full pass over the
README and every guide, covering everything shipped since v0.62 — group
rollups, the audit log, quiet hours, branding, and accounts. The README's
feature list was a flat wall of 35 bullets and is now grouped by what someone
is actually trying to find out. -
Six regenerated screenshots, captured against the current UI over a seeded
install rather than whatever happened to be on screen.
Changed
-
Upgrading is automatic. The single operator in the pre-v0.70
admin_credentialsrow becomes the owner on first boot, password digest
carried across rather than re-hashed, so the same credentials keep working.
The legacy table is left in place so a downgrade still finds what it expects.
Verified end to end by building v0.67.1, writing services, branding and a
quiet-hours schedule through it, then booting this version on that database. -
PUT /api/auth/credentialschanges your own account, and now requires a
signed-in session rather than merely a credential. A caller holding only
LANTERN_AUTH_TOKENhas no account to change — letting a machine credential
rewrite a person's password is exactly what/api/admin/*refuses. It also
revokes only your sessions, not everyone's. -
LANTERN_AUTH_TOKENauthenticates as admin, never owner. A static string
in an environment variable should be able to run the installation, not mint
accounts on it. -
Audit entries name the acting user instead of a shared
"admin".
Fixed
-
loadData()threw aReferenceErroron every 30-second poll. The v0.67.0
brand commit removed thepulsebinding but left a use of it in thefinally
block, so the header's "last updated" clock stopped advancing and the console
filled up. Present in v0.67.0 and v0.67.1. -
Account management is refused on an install with no credentials. Such an
install is wide open by design, so without this any passer-by couldPOST
themselves an owner and lock the real operator out. -
.btnhad no:disabledstyling, though every other control does. A
button the app had deliberately turned off looked exactly like one that
worked.
v0.67.1 — Branding logo blocked by our own CSP
Fixed
- A branding
logo_urlon another host never loaded. Settings accepted it
and the API stored it, but the response CSP isimg-src 'self' data:, so the
browser refused the image — the feature was only ever going to work for a
same-origin logo.img-srcnow also carries the single origin of the
configured logo (scheme and host only, not its path), primed at startup and
refreshed on every branding change. Nothing else is allowed: a blanket
https:would have opened every install to every image host to support a
setting most installs never touch.
Added
- docs/CUSTOM_DOMAIN.md — serving the status page
on your own hostname, with Caddy/nginx/Traefik snippets, how to expose only
/statuson a public hostname, and the four things that change behind a
proxy:X-Forwarded-Protois required forSecuresession cookies, the
login throttle and the audit log both see the proxy's address rather than the
visitor's, and aHost-rewriting proxy needsLANTERN_WS_ALLOWED_ORIGINS.
v0.67.0 — New brand mark
Changed
- A real lantern for a logo. The favicon and PWA icon were two unrelated
marks: a red Lucide "flame-kindling" outline (#e32400) and an amber flame
on a rounded square (#f59e0b). Both were painted in status colours — on a
monitoring dashboard a red tab icon reads as "something is down" at a
glance, and amber is--degraded. They are now one mark, drawn once and
used everywhere: a lantern housing in slate with an emerald-lit glass panel
and a white flame. - The header has a logo. The pulsing emerald dot beside the wordmark is
replaced by the lantern mark, whose flame flickers instead (and holds still
underprefers-reduced-motion, via the existing global rule). Its housing
follows the theme's text colour so it reads on light and dark; the glass
keeps the logo's own fixed emerald rather than following the accent picker,
since a light custom accent would swallow the white flame drawn on it. A
customlogo_urlfrom Settings → Branding replaces the mark rather than
sitting beside it. - The
🔆in the browser tab title is gone — the favicon does that job now.
Removed
- The
.loading-pulseindicator and itspulsekeyframes. Its.loading
state was only ever removed, never set, so it was a permanently-pulsing dot
with no meaning.
v0.66.0 — Status page branding
Added
-
GET/PUT /api/branding(plus the unauthenticated public mirror
GET /api/public/branding), and a new "Branding" section in
Settings → General. Overrides three things in the header, on both the
dashboard and the public status page:- Name — replaces "Lantern" in the header and the browser tab title.
- Logo URL — an image shown beside the name. Restricted to
http(s)
URLs server-side, so a stored value can never become ajavascript:or
data:URI reflected into an<img src>. - Default accent — a hex color applied to visitors who have not
picked their own accent. A personal choice always wins over the
operator default.
Writes are admin-only: a per-service scoped API token gets a 403, since
branding is installation-wide. Every change is recorded in the audit log.
Closes the "public status page cannot be branded" feature gap.Custom domains stay a reverse-proxy concern rather than application
state — point a hostname at Lantern's/statusand the branding follows.
Fixed
- The browser tab title had a hardcoded
v0.62.2in it, five releases
stale. It now reads "Lantern — Service Dashboard" (or the configured
name).
v0.65.0 — Notification quiet hours (mute or digest)
Added
-
A global, scheduled quiet-hours window for notifications —
GET/PUT /api/notifications/schedule, and a new "Quiet Hours" section
in Settings → Alerts & Webhooks. Configure a daily UTC start/end time
(wrapping past midnight is fine, e.g. 22:00-08:00) and a mode:- Mute: notifications during the window are dropped, the same way
per-service maintenance mode already works, just time-scheduled and
global instead of manual and per-service. - Digest: notifications during the window are queued instead of
dropped, then sent as one combined message per channel — respecting
each service's existing alert routing — as soon as the window closes.
A background flusher checks every minute.
This sits alongside, not instead of, per-service maintenance mode: both
are independent suppression paths into the same notification dispatch.
Closes the "only binary maintenance mode; no scheduled quiet hours or
batched digest" feature-gap. - Mute: notifications during the window are dropped, the same way
v0.64.0 — Admin action audit log
Added
GET /api/admin/audit-log: a persisted record of admin actions —
logins (success and failure, with source IP), credential changes, service
deletion, group/alert-route/monitor edits, maintenance mode toggles,
webhook channel changes (channel names only, never the URLs), config
imports, and Docker container restarts. Each entry records who (the
session/admin-token caller as"admin", or a scoped token as
"token:<service>"), what, the target, success/failure, source IP, and
timestamp. Closes the "login throttling is in-memory only, no persisted
record of login attempts or config changes" gap from the last audit.
Admin-only — seeisAdminOnlyEndpointbelow. Viewable from the
Diagnostics drawer's new "Audit Log" tab.
Fixed
- Scoped API tokens could disable another service's maintenance mode
(High).PUT /api/services/{name}/maintenancewas the one mutating
per-service handler without a scoped-token ownership check — the same
class of gap Fix 1 (v0.62.4) closed for the monitor endpoints. A token
issued to service A could silence service B's alerts by flipping B into
maintenance mode, or falsely mark it under maintenance to mask a real
outage. Found while wiring the audit log's own admin-only gating; fixed
the same way as the earlier monitor-handler fix.
v0.63.1 — Scoped API tokens could authenticate against admin-only routes
Fixed
- Privilege escalation via scoped API tokens (High). A token issued to
one service authenticated successfully as a Bearer credential on every
route the auth middleware saw — includingGET /api/backup(a full
database snapshot containing the admin credential hash, every session
hash, and every other service's API token),/api/webhooks
(Discord/Telegram webhook URLs),/api/config/exportand
/api/config/import(the whole installation's configuration), and
PUT /api/auth/credentials. Earlier scoped-token checks (v0.62.4) only
stopped a token from acting on a service other than its own; nothing
stopped it from acting as a full admin.isAdminOnlyEndpointnow
enumerates these routes andauthMiddlewarerejects a scoped token
against them with 403 before it ever reaches the handler. The admin-wide
bearer token, session cookie, and Basic Auth paths are unaffected.
v0.63.0 — Service-group status rollup
Added
GET /api/groups(and its public counterpart) now returns an aggregate
health rollup per group, not just a member count.GroupSummarygained
status(the worst status across the group's member services — down beats
degraded beats up beats unknown, the same ranking the heartbeat strip uses
to pick a bucket's dominant status) andmaintenance(true if any member
currently has maintenance mode enabled). The dashboard's collapsible group
headers show a live status dot driven by this rollup, computed the same
way client-side fromstate.servicesso it updates in real time over the
WebSocket feed without an extra poll; a group with anything down or
degraded always shows that, even if others in it are paused for
maintenance.