Skip to content

Releases

v1.5.2 — Console favicon, demo animations & S3 UI

Choose a tag to compare

@garyhooi garyhooi released this 13 Nov 10:49

What's new

Branded console favicon

The hosted console was still pointing at Vite's default /vite.svg, which no longer exists in the repository. Browsers therefore showed no icon in the tab/title bar.

This release adds the same ShieldLock favicon used by the demo:

  • frontend/public/icon.svg as the SVG tab icon
  • frontend/public/apple-icon.png for iOS home-screen / pinned tabs
  • theme-color set to the console's dark background

Static assets are baked into the console image, so an existing deployment must rebuild the console container to pick this up. Browsers may also cache the old missing favicon; a hard refresh or reopening the tab clears it.

Restored demo animations

The interactive demo now matches the real console's motion language again:

  • Login: the sign-in button hands off to a full-page "identity verified" hologram (rings, scan sweep and checkmark) before the dashboard opens. The login card also gets a subtle entrance animation.
  • Logout: signing out shows the full-page "session terminated" power-down animation before returning to the login screen.
  • Loading: backup create/restore/delete and S3 save/test/schedule actions now show the teal hologram loading overlay. Demo view changes also animate in.

S3 backup in the demo

  • Added Configuration → Backup storage to the demo:
    • S3 endpoint, region, bucket and prefix
    • Access key / secret and path-style option
    • Save and test-connection states
    • Required S3 permissions note
    • Automatic backup interval and retention controls
  • The demo Backups screen now shows Encrypted/Plain, Verified, S3 copies, and automatic schedule/retention state.
  • The demo audit log uses the real wgconsole_backup_*.sql.gz naming and marks the S3 upload.
  • Updated the landing backup copy, FAQ, feature card and structured data to describe verified, encrypted, optional-S3 backups with automatic schedules and retention.
  • README backup documentation now covers S3 copies, manual deletion of both copies, retention pauses and the API-process scheduler.

Upgrade notes

  • No database migration.
  • No API or permission changes in this release.
  • Rebuild/redeploy the console container for the favicon.
  • Redeploy the landing site for the demo animations and S3 screens. If Cloudflare Pages is connected to main, that deployment should trigger automatically.

Verification

  • Console frontend production build passes; dist/icon.svg and dist/apple-icon.png are generated and referenced by dist/index.html.
  • Landing tsc --noEmit passes.
  • Landing next build passes (10/10 static pages).
  • Browser-checked on a local production build: login success animation, logout animation, loading overlay, S3 backup settings tab and the S3 backup table.

v1.5.1 — Delete S3 copies with backups

Choose a tag to compare

@garyhooi garyhooi released this 13 Nov 09:07

Fix

Manual backup deletion now removes the S3 object too.

In v1.5.0, deleting a backup from System → Backups only removed the local archive. The S3 copy was left behind unless retention deleted it later. That was a bug in the manual delete path.

This patch:

  • Reads the backup's recorded S3 key before deleting it.
  • Deletes the S3 object first, then removes the local archive. If the S3 copy cannot be removed, the local backup is kept and the API returns an error instead of silently leaving an orphaned remote object.
  • Still works when new S3 uploads are currently disabled, as long as the stored S3 destination and credentials can still delete the recorded object.
  • Updates the confirmation prompt, success message, and audit log to show that the S3 copy was removed.
  • Adds E2E coverage proving a manual delete removes the object from a fake S3 bucket as well as the local archive.

The behavior is now:

  1. Delete the S3 object recorded on the backup sidecar.
  2. Delete the local archive.
  3. If step 1 fails, keep the local archive and show the S3 error so it can be fixed and retried.

Important: already-orphaned objects

If you already deleted the local archive under v1.5.0, its local sidecar and filename are gone, so this patch cannot identify that existing S3 object automatically. Delete that one object once in your S3 console or with the CLI, for example:

aws s3 rm s3://YOUR_BUCKET/wireguard-console/backups/OBJECT_NAME

For MinIO, the equivalent is mc rm. After upgrading, deleting any backup that still appears in the console will remove both copies.

Upgrade notes

  • No database migration is required.
  • Upgrading replaces the API binary/container; existing S3 configuration and backups are untouched.
  • This patch only affects manual deletion. Automatic retention already deleted matching S3 objects.

Verification

  • Full E2E against PostgreSQL 18 with a fake S3 endpoint: create an S3-backed backup, manually delete it, assert both the local file and the S3 object are gone.
  • Backend: go test ./... and go vet ./... green.
  • Frontend: 273 tests green; tsc -b and production Vite build clean.

v1.5.0 — S3 backup storage & automatic retention

Choose a tag to compare

@garyhooi garyhooi released this 13 Nov 08:21

What's new

S3-compatible backup destinations

Configuration → Backup storage now configures a generic S3 destination:

  • Works with AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, and other S3-compatible providers.
  • Configurable endpoint (blank for AWS), region, bucket, object prefix, access key ID, secret access key, session token, and force-path-style.
  • Static credentials are encrypted with APP_ENCRYPTION_KEY and are never returned by the API. Leave the access key and secret blank to use the API container's AWS credential chain.
  • Test connection checks HeadBucket plus a temporary put/get/delete under the configured prefix, so it verifies the permissions a backup needs.
  • After every new backup passes its automatic restore check, it is uploaded to the bucket. The local archive remains on the console volume for the one-click restore list; the S3 object is the off-site copy.
  • Existing backups are not uploaded retroactively; create a new backup after enabling S3.

Automatic backups and retention

  • Choose an automatic interval: hourly, every 6/12 hours, daily, every 3 days, weekly, or every 30 days.
  • Choose how many backups to keep (0–100). After a successful backup, older local archives and their matching S3 objects are removed automatically.
  • Retention pauses when the new backup fails verification or its S3 upload fails, so older usable copies are not deleted while the current one is only local or unverified.
  • Unverified backups are not uploaded to S3 and are not used to trigger retention.
  • Manual creation, scheduled creation, restore, upload-restore, and deletion are serialised, avoiding a backup running against a half-restored database.
  • The scheduler runs in the API process and checks once per minute; the API must be running for scheduled backups to occur.

Backup list

  • Backups uploaded to S3 now show an S3 badge alongside the encrypted/plain and verification state.
  • The create flow reports S3 upload warnings and retention warnings without hiding a backup that was successfully created locally.
  • The staged create animation now includes the S3 upload stage.

Required S3 permissions

Replace the bucket and prefix as needed:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::YOUR_BUCKET"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts"
      ],
      "Resource": "arn:aws:s3:::YOUR_BUCKET/wireguard-console/backups/*"
    }
  ]
}

For non-AWS providers, grant the equivalent list/read/write/delete permissions on the bucket and prefix. Large backups use multipart upload, so include the multipart abort permission. Use an HTTPS endpoint outside a trusted local network; plain HTTP sends the access key and secret in cleartext.

Upgrade notes

  • Migration 034 applies automatically on the next API start. It adds the S3 destination, automatic-backup interval, retention count, and last-attempt columns to the existing config row.
  • S3 is optional. Leaving it disabled preserves the previous local-only backup behaviour.
  • New backups only: existing local archives are not uploaded automatically. Create a new backup after enabling S3 if you want a remote copy.
  • S3 credentials are stored encrypted with APP_ENCRYPTION_KEY; keep that key with your other deployment secrets.
  • The console keeps a local archive after uploading. If the console volume is lost, download the object from S3 and use Restore from upload.
  • Automatic backups do not catch up missed intervals after downtime; the next attempt runs after the API restarts if the interval has elapsed.

Verification

  • Backend: go test ./... and go vet ./... green.
  • Full E2E against PostgreSQL 18 with a fake S3 endpoint: S3 save/test, backup upload, remote retention deletion, and the existing backup/restore flows green.
  • Scheduled-backup path exercised against a temporary PostgreSQL database: archive created, restore-verified, and backup_last_auto_at updated.
  • Frontend: 36 test files / 273 tests green; tsc -b clean; production Vite build clean.

v1.4.1 — Firewall guidance for console & exit nodes

Choose a tag to compare

@garyhooi garyhooi released this 13 Nov 07:00

Documentation and landing update

This release is documentation-only — no API, database, or console behaviour changed.

  • README firewall matrix clarified. The console-host table now says port 53 is console-only, and a dedicated Exit node ports table documents what a node actually needs:
    • 51820/udp inbound (or the custom WireGuard port) for peers
    • 443/tcp outbound for the agent's console polling
    • 53/udp+tcp outbound only when peers use the console's AdGuard resolver
    • 22/tcp inbound for the one-time install only
  • Exit nodes do not need inbound 80/443. Cloud-firewall guidance now distinguishes console-host rules from node rules and calls out node egress on 443.
  • New landing Firewall section at /#firewall, with console-host and exit-node port cards, inbound/outbound/internal labels, the private 3000/tcp warning, and cloud-firewall guidance.
  • Navigation: Firewall added to the header/mobile navigation and footer.

Upgrade notes

  • No migrations, configuration changes, or restarts are required for the application itself.
  • If you self-host the landing site, redeploy it to publish the new /#firewall section.

Verification

  • landing: tsc --noEmit clean.
  • landing: next build clean (10/10 static pages generated).
  • README and landing content reviewed for consistent console-host vs exit-node port guidance.

v1.4.0 — Encrypted, verified backups & flexible roles

Choose a tag to compare

@garyhooi garyhooi released this 13 Nov 06:45

What's new

Encrypted, verified backups

  • Backup passphrase. Configuration → Security → Backup encryption can now set a passphrase used automatically for every new backup, regardless of which admin creates it. The passphrase is stored encrypted with APP_ENCRYPTION_KEY and is never returned by the API.
  • Self-contained encrypted archives. New backups are written as authenticated .sql.gz.enc files using scrypt + ChaCha20-Poly1305. A downloaded file plus the passphrase is sufficient to recover it; the passphrase is not embedded in the file.
  • Passphrase changes do not strand old backups. Up to 10 previous passphrases are retained encrypted, so archives made before a change (or before encryption was disabled) remain restorable. Existing plain .sql.gz backups continue to restore normally.
  • Automatic restore verification. Every new backup is restored into a temporary database before it is reported as successful. Failures keep the archive but mark it Unverified with a visible warning, rather than treating an unusable dump as a good backup.
  • Backup list improvements. The page now shows Size, Protection (encrypted/plain), and Verification columns, with a dedicated staged loading animation while the backup is dumped, encrypted, and verified.
  • Upload/download/restore support. Encrypted archives can be downloaded, uploaded back, and restored through the same MFA-gated flows.

More flexible roles

  • Only super_admin remains a locked system role. admin and auditor are now ordinary seeded roles: they can be edited, renamed, and deleted once no admin is assigned to them.
  • Deleted defaults stay deleted. Role seeding runs once behind a migration marker (migration 033), so restarting or upgrading the console will not recreate Admin/Auditor after deletion.
  • Sensitive capabilities are assignable. Access-control, security-policy, audit-purge, and maintenance permissions can now be granted to custom roles. The * wildcard remains reserved for super_admin.
  • Capability-aware access screens. A roles.manage-only role sees the roles workspace without the admin list; admins.view is read-only; security.manage can edit security policy. Role and group controls are hidden or disabled when the acting admin lacks the corresponding capability.
  • Invite fallback. If the Admin role has been deleted, an invite that omits a role now falls back to the first remaining non-super role instead of failing.

Upgrade notes

  • Migrations 031, 032, and 033 apply automatically on the next API start. They add the encrypted backup-passphrase columns, make Admin/Auditor non-system roles, and seed the two default roles only once. No manual SQL is required.
  • Existing backups are untouched. Plain archives remain restorable; encryption applies to newly created backups after enabling the setting.
  • To delete Admin or Auditor while it still has admins assigned, reassign those admins first; the console intentionally refuses an in-use role deletion.
  • Automatic verification requires the database user to be able to create/drop databases (CREATEDB). The default Docker PostgreSQL superuser has this. Without it, backups are still created but shown as unverified with an explanatory warning.
  • Keep the backup passphrase somewhere safe. If both the server and its database are lost, the passphrase is the only way to open an encrypted archive.

Verification

  • Backend: go test ./... and go vet ./... green.
  • Full E2E against a temporary PostgreSQL 18 cluster: backup creation/verification, plain and encrypted download, restore, upload restore, passphrase enable/disable, and restore with a retained passphrase after disabling.
  • Frontend: 36 test files / 272 tests green; tsc -b clean; production Vite build clean.
  • Migrations re-applied twice in a scratch database to confirm idempotency and that deleted Admin/Auditor roles are not recreated.

v1.3.0 — Custom admin roles, policies & groups

Choose a tag to compare

@garyhooi garyhooi released this 13 Nov 04:57

What's new

Admin access control is now data, not a hardcoded list.

  • Editable roles and policies. The console ships a fixed capability catalogue (monitoring, network, directory, rules, system, backups, and reserved access-control/security/maintenance capabilities). Create custom roles and assign each admin exactly one role.
  • Built-in roles are editable in place. admin and auditor can be renamed, re-described, and given a different permission policy; every admin assigned to them picks up the change on their next request — no re-invite or reassignment. super_admin remains locked to full access as the break-glass role.
  • Custom roles cannot take reserved capabilities. Access control, security policy, audit purge, and maintenance stay super-admin-only.
  • Admin groups. Groups are organizational only: they make the admin list filterable and support bulk role assignment for every member. Each admin can belong to many groups. Invite forms accept groups, and the admin list shows group chips and a group filter.
  • Permission-aware console. API routes are guarded by capabilities, and the UI filters navigation and write actions accordingly. Existing admin users keep their operational access; auditor becomes genuinely read-only instead of having no usable routes.
  • MFA and audit coverage. Role creation/editing/deletion and bulk group role assignment require a fresh MFA step-up. Role and group changes are audited, and last-super-admin guards prevent lockouts.

Safer admin account changes

  • Changing an admin's email now resets the passkeys bound to that account. The console warns first and requires an explicit confirmation before any MFA step-up is consumed; the email change and WebAuthn reset are atomic. Authenticator-app codes and recovery codes keep working, and other sessions are signed out.
  • Added guards against deleting the last active super admin or deleting your own admin account.

Upgrade notes

  • No manual database steps. Migration 030 is additive and idempotent and applies automatically on the next API start. It seeds the three built-in roles, replaces the old three-value role check with a foreign key, and creates the admin-group tables.
  • Existing admins keep their current role slug. Policy edits made later are preserved across restarts and upgrades.
  • Admin email changes are now a passkey-reset action. If SMTP/email changes are part of your workflow, expect the explicit confirmation prompt and re-enrollment of passkeys; TOTP and recovery codes are unaffected.

Verification

  • Backend: go build ./..., go vet ./..., and go test ./... green.
  • Migration 030 applied twice against a temporary PostgreSQL 18 cluster; schema, seeded policies, and the admins.role foreign key verified.
  • Frontend: 36 test files / 266 tests green, tsc clean, production Vite build clean.
  • New tests cover the capability catalogue/canonicalisation, permission middleware, role creation, group creation, MFA-gated bulk role assignment, permission filtering, and the admin email/passkey-reset flow.

v1.2.0 — Passwordless passkey sign-in

Choose a tag to compare

@garyhooi garyhooi released this 02 Nov 07:18

What's new

Sign in with a passkey — no password required.

  • Passwordless passkey sign-in. The login page now has a single Sign in with a passkey button. It opens the browser's discoverable-credential picker (Touch ID / Face ID / Windows Hello / security key) and signs the admin straight in — no email, no password, no second step. The passkey itself is the multi-factor credential (possession + biometric/PIN).
  • Password sign-in is unchanged and still MFA-enforced. Entering a password only ever creates a pending login; a second factor (passkey, authenticator app, or a one-time recovery code) is mandatory before a session is issued, and accounts with no factor are still forced through enrollment first.
  • New passkeys are discoverable by default (resident keys required), which is what enables one-tap sign-in. Existing passkeys keep working as a normal second factor; re-add one from Profile if it was registered before this release and you want it to work passwordless.
  • Capability-gated, never blocking. The passkey button is hidden on IP-only installs (WebAuthn needs a domain) and on browsers without WebAuthn support; the password form always stays available. Cancel/timeout shows a short hint; rate-limited attempts reuse the existing lockout countdown.

Security

  • Passwordless assertions require user verification and are bound to an HttpOnly ceremony cookie plus a single-use, 5-minute challenge row claimed atomically.
  • The account is resolved from the assertion's WebAuthn user handle (webauthn_users), never from request input; unknown/disabled/invalid all return the same generic 401 (no enumeration).
  • IP/account lockout counts failed passwordless attempts and resets only after a completed login; audit event mfa.login_passkey records {"passwordless":true}.
  • Sessions are minted only through the shared mintLoginSession chokepoint — the same path used by MFA and enrollment.

Upgrade notes

  • No manual database steps. Migration 029 (nullable passwordless challenges) is additive and idempotent; the API applies it on boot and the normal one-line installer update picks it up.
  • Existing passkeys from v1.1.0 were registered without the resident-key requirement and may not be discoverable. The password + MFA path is unaffected; add a fresh passkey from Profile → Add passkey to enable passwordless sign-in.
  • IP-only deployments are unchanged: no passkey button, authenticator-app MFA continues to work.

Verification

  • Backend: build + vet clean; unit suites green; full e2e against PostgreSQL 18 in 69.8 s — including 5 new passwordless suites (positive flow, password still pending_mfa, residentKey=required, all 401 negatives — missing cookie / replayed challenge / unknown handle / wrong origin / disabled — and IP lockout).
  • Frontend: 33 files / 254 tests green, tsc + production build clean.
  • Migration 029 re-applied cleanly on an existing database (idempotency verified).
  • Browser demo (real Chrome + CDP virtual authenticator): 7/7 flows, zero console errors — passkey button visible, forced enrollment, password still asks for MFA, then passwordless sign-in to the dashboard.

v1.1.0

Choose a tag to compare

@garyhooi garyhooi released this 31 Oct 19:05

What's new

Multi-factor authentication replaces 2FA — and passkeys are now a first-class sign-in factor.

  • Passkeys (WebAuthn). Enroll a passkey (Touch ID / Windows Hello / security key) alongside or instead of an authenticator app, then confirm with it at sign-in and on sensitive actions. Passkey step-up authorizes one sensitive action per verification. On IP-only installs (no domain) passkeys are unavailable by design — authenticator-app MFA keeps working, and the installer summary tells you which mode applies.
  • Mandatory MFA enrollment. The password step now only starts a login attempt. An admin with no factor is forced through enrollment before any session is issued; every later sign-in requires a passkey/authenticator code, with one-time recovery codes as fallback. Configuration → Security can require any (default), totp, passkey, or both; passkey-requiring policy safely degrades on IP-only deployments instead of locking anyone out.
  • Recovery codes. 12-character, single-use, salted at rest, and now accepted at sign-in (not just step-up). Regenerate them from Profile after a fresh MFA check. A super admin can reset another admin's MFA — every factor is cleared, their sessions are revoked, and they re-enroll at next sign-in.
  • MFA management UI. Profile lists factors with last use, adds/removes passkeys and the authenticator app (step-up gated), shows remaining recovery codes, and warns when an account was left without a factor. The step-up dialog offers "Use passkey" or an authenticator/recovery code.
  • 2FA → MFA rename. Routes (/mfa-verify, /mfa-setup), JSON fields (pending_mfa, is_pending_mfa), cookie (wgc_mfa_pending), audit actions and all user-facing copy.

Security & correctness

  • Login can never mint a session without a proven factor; the session middleware rejects pending login rows.
  • MFA attempts are bounded per pending login (5 failures → 429 + lockout), and the IP brake resets only after a completed MFA login.
  • Enrollment secrets are server-generated and stored encrypted; WebAuthn challenges are cookie-bound, single-use and short-lived.
  • WebAuthn credential flags (including backup-eligible) are persisted raw and restored correctly; sign-count/clone-warning are written back after each login.
  • Adding/removing factors or resetting MFA revokes other sessions and clears step-up stamps.
  • Backup restore re-runs migrations so the new MFA tables exist immediately.

Dependencies

  • Added github.com/go-webauthn/webauthn v0.18.2 (BSD-3) and @simplewebauthn/browser 14.0.0 (MIT, zero dependencies, dynamically imported).
  • Upgraded Go modules (pgx 5.12, x/net 0.62, WireGuard) and frontend packages to their latest releases.

Upgrade notes

  • No manual database steps. Migration 028 is additive and runs on boot. Existing authenticator-app users keep their factors; the first sign-in after upgrading shows the new MFA challenge.
  • Breaking API rename: the old /api/auth/2fa/* endpoints and 2FA JSON fields are gone (no aliases, by design). A tab left open mid-login across the upgrade must sign in again; active sessions survive.
  • Passkeys need a real domain. They are unavailable on IP-only installs. Changing CONSOLE_DOMAIN later orphans previously registered passkeys — the installer now warns about this (authenticator-app MFA and recovery codes keep working).
  • VERSION is 1.1.0; the one-line installer picks it up automatically.

Verification

  • Backend: build + vet clean; go test ./internal/... green (13 MFA unit tests, 7 WebAuthn unit tests); full e2e against PostgreSQL 18 13/13 suites green — including passkey enrollment/login, one-shot passkey step-up, recovery-code login single-use, last-factor guard, reset-revokes-sessions and 5×wrong-code → 429.
  • Frontend: 33 files / 236 tests green, tsc + production build clean.
  • Installer/shell tests green; browser demo exercised 12/12 flows end-to-end (forced passkey enrollment → passkey login → add authenticator app via passkey step-up → authenticator login → recovery-code login → regenerate codes) with no console errors.

v1.0.49

Choose a tag to compare

@garyhooi garyhooi released this 27 Oct 04:06

What's fixed

Peer creation no longer hangs the request on the WireGuard kernel apply — and a retry can no longer create a duplicate peer.

Creating a peer committed the row, then pushed the whole server state to wg-helper before answering. On a slow apply the browser or proxy timed out first: Safari showed Load failed for a peer that was already in the console, and clicking Create again inserted a second peer.

  • Peer / server / claim / user-suspend mutations now commit and answer immediately. The kernel apply runs in a coalesced per-server background queue (one pass per server at a time, re-reading the newest state; a deleted server's interface teardown is ordered after any in-flight apply).
  • POST /api/peers accepts an optional request_id idempotency key, auto-migrated on the first API boot (metadata-only column + partial unique index). A retry replays the original peer (200 {"status":"exists"}) instead of inserting or emailing again; overlapping duplicate requests collapse to one row.
  • Add Peer UI: one stable request id per dialog (reused by retries), a truthful "the request got no response — the peer may already have been created; retrying is safe" hint instead of the raw browser error, an automatic peers-list refresh, and success shown as a notice rather than the red error line.

Measured with a deliberately slow 3 s apply: POST /api/peers 3.004 s → 1.1 ms, and a retry that previously created 2 DB rows now returns the original peer (1 row).

Upgrade with the usual installer (console rebuild; migration 027 is applied automatically on API start):

curl -fsSL https://raw.githubusercontent.com/garyhooi/wireguard-console/main/install.sh | sudo bash

Verified: backend build/vet/unit + full E2E suite (twice, including slow-apply latency, idempotent retry, overlapping duplicates and async teardown), -race on the backend and E2E, frontend 140/140 tests + production build, installer/firewall/TLS/AdGuard shell suites.

v1.0.48

Choose a tag to compare

@garyhooi garyhooi released this 23 Oct 17:30

What's fixed

The installer asks again — so the domain and the Cloudflare answer can be changed on any run. And it no longer gets "Automatic SSL/TLS" wrong.

  • Every interactive installer run now prompts for the console address and for the Cloudflare SSL/TLS mode. Both prompts show the stored values as defaults, so Enter keeps the current setup; typing a new domain or changing the y/n answer updates .env (the WireGuard endpoint follows a domain change, and peers are reminded to re-download their configs). Non-interactive runs keep the stored values; CONSOLE_DOMAIN=... / WGCONSOLE_TLS=... override them for automation.
  • The Cloudflare question now asks about Flexible only: Flexible is the only mode that needs a plain-HTTP origin (Caddy on :80, no TLS). Full, Full (strict) and Automatic SSL/TLS all expect the HTTPS origin that domain mode provides — Automatic probes the origin over HTTP and HTTPS and only ever upgrades the encryption mode, it never downgrades, so a plain-HTTP origin would prevent the upgrade. Default answer is N.
  • New installs default to domain mode: Caddy obtains a public certificate and serves HTTPS on :443. WGCONSOLE_TLS=domain is an explicit, documented value; unknown legacy values fall back to domain mode instead of the IP-only listener.
  • After the stack starts, a public-URL probe warns with the exact fix when it detects the Flexible redirect loop (it never changes configuration by itself).
  • New scripts/tls-mode.sh (classification + resolution helpers) with tests, wired into the local test runner.

Upgrade with the usual installer (console rebuild); it will ask the two questions and keep your current values on Enter:

curl -fsSL https://raw.githubusercontent.com/garyhooi/wireguard-console/main/install.sh | sudo bash

Verified: tls-mode tests, firewall suite, AdGuard healthy-skip test, bash -n on every installer script, entrypoint sh -n.