Skip to content

Releases: ManasvinYadav/lantern

v0.72.0 — Tokens, sessions, 2FA, dependencies and long-range history

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 08 Sep 04:02

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-wide LANTERN_AUTH_TOKEN instead.

    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=1 on 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_DAYS a 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 under skipped, 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 one webhook_deliveries row 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-For was ignored, so everything behind a reverse proxy shared one throttle
    bucket.
    Keying the login throttle on RemoteAddr is 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_PROXIES takes 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. findDockerContainer falls 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 called db, and app suffix-matches both web-app and
    worker-app. The caller of this is POST /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/status creates 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

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

Fixed

  • The listening on :7654 (auth=...) line reported LANTERN_AUTH_USER, not the
    gate authMiddleware actually enforces. An install whose operator signs in
    with a stored account — every install upgraded to v0.70.0 — logged
    auth=false on 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

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

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/backup would hand them every password
    hash, GET /api/webhooks every 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/session now reports role, 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_credentials row 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/credentials changes your own account, and now requires a
    signed-in session rather than merely a credential. A caller holding only
    LANTERN_AUTH_TOKEN has 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_TOKEN authenticates 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 a ReferenceError on every 30-second poll. The v0.67.0
    brand commit removed the pulse binding but left a use of it in the finally
    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 could POST
    themselves an owner and lock the real operator out.

  • .btn had no :disabled styling, 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

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

Fixed

  • A branding logo_url on another host never loaded. Settings accepted it
    and the API stored it, but the response CSP is img-src 'self' data:, so the
    browser refused the image — the feature was only ever going to work for a
    same-origin logo. img-src now 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
    /status on a public hostname, and the four things that change behind a
    proxy: X-Forwarded-Proto is required for Secure session cookies, the
    login throttle and the audit log both see the proxy's address rather than the
    visitor's, and a Host-rewriting proxy needs LANTERN_WS_ALLOWED_ORIGINS.

v0.67.0 — New brand mark

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

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
    under prefers-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
    custom logo_url from 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-pulse indicator and its pulse keyframes. 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

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

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 a javascript: 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 /status and the branding follows.

Fixed

  • The browser tab title had a hardcoded v0.62.2 in it, five releases
    stale. It now reads "Lantern — Service Dashboard" (or the configured
    name).

v0.65.0 — Notification quiet hours (mute or digest)

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

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.

v0.64.0 — Admin action audit log

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

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 — see isAdminOnlyEndpoint below. 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}/maintenance was 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

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

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 — including GET /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/export and
    /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. isAdminOnlyEndpoint now
    enumerates these routes and authMiddleware rejects 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

Choose a tag to compare

@ManasvinYadav ManasvinYadav released this 04 Sep 05:15

Added

  • GET /api/groups (and its public counterpart) now returns an aggregate
    health rollup per group
    , not just a member count. GroupSummary gained
    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) and maintenance (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 from state.services so 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.