- Replace HSL-based indigo theme with official shadcn violet OKLCH theme
in globals.css for proper contrast in both light and dark mode
- Update tailwind.config.ts to use var(--...) instead of hsl(var(--...))
for OKLCH color space compatibility
- Fix Radix UI crash: replace SelectItem value="" with "__none__" sentinel
in HostDialogs.tsx and L4HostDialogs.tsx (empty string value is invalid)
Form action parsers already return null for non-numeric values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 24 shadcn/ui component files to src/components/ui/ via the shadcn CLI, installs required @radix-ui/* and related dependencies, and updates components.json aliases to resolve under src/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused Box/Collapse from L4PortsApplyBanner
- Remove unused Stack from RedirectsFields
- Remove unused updateL4ProxyHost import from validation test
- Add eslint-disable-next-line for require() in vi.hoisted() blocks
(necessary pattern since vi.hoisted runs before ESM imports)
- Add file-level eslint-disable no-explicit-any for test files that
intentionally pass invalid types to test validation logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Banner (L4PortsApplyBanner):
- Accept refreshSignal prop; re-fetch /api/l4-ports when it changes
- Signal fires immediately after create/edit/delete/toggle in L4ProxyHostsClient
without waiting for a page reload
Master-slave replication (instance-sync):
- Add l4ProxyHosts to SyncPayload.data (optional for backward compat
with older master instances that don't include it)
- buildSyncPayload: query and include l4ProxyHosts, sanitize ownerUserId
- applySyncPayload: clear and re-insert l4ProxyHosts in transaction;
call applyL4Ports() if port diff requires it so the slave's sidecar
recreates caddy with the correct ports
- Sync route: add isL4ProxyHost validator; backfill missing field from
old masters; validate array when present
Tests (25 new tests):
- instance-sync.test.ts: buildSyncPayload includes L4 data, sanitizes ownerUserId;
applySyncPayload replaces L4 hosts, handles missing field, writes trigger
when ports differ, skips trigger when ports already match
- l4-ports-apply-banner.test.ts: banner refreshSignal contract + client
increments counter on all mutation paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New l4_proxy_hosts table and Drizzle migration (0015)
- Full CRUD model layer with validation, audit logging, and Caddy config
generation (buildL4Servers integrating into buildCaddyDocument)
- Server actions, paginated list page, create/edit/delete dialogs
- L4 port manager sidecar (docker/l4-port-manager) that auto-recreates
the caddy container when port mappings change via a trigger file
- Auto-detects Docker Compose project name from caddy container labels
- Supports both named-volume and bind-mount (COMPOSE_HOST_DIR) deployments
- getL4PortsStatus simplified: status file is sole source of truth,
trigger files deleted after processing to prevent stuck 'Waiting' banner
- Navigation entry added (CableIcon)
- Tests: unit (entrypoint.sh invariants + validation), integration (ports
lifecycle + caddy config), E2E (CRUD + functional routing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Production (Docker): src/lib/db.ts now uses bun:sqlite + drizzle-orm/bun-sqlite.
No native addon compilation needed — bun:sqlite is a Bun built-in. The Dockerfile
drops all native build tools (python3, make, g++) and uses --ignore-scripts.
Tests (Vitest/Node.js): bun:sqlite is unavailable under Node.js, so:
- tests/helpers/db.ts keeps better-sqlite3 + drizzle-orm/better-sqlite3 for
integration tests that need a real in-memory SQLite
- vitest.config.ts aliases bun:sqlite → a thin better-sqlite3 shim and
drizzle-orm/bun-sqlite → drizzle-orm/better-sqlite3 for unit tests that
transitively import src/lib/db.ts without executing any queries
- better-sqlite3 stays as a devDependency (test-only, not built in Docker)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds two new UI-configurable Caddy patterns that previously required raw JSON:
- Per-path redirect rules (from/to/status) emitted as a subroute handler before
auth so .well-known paths work without login; supports full URLs, cross-domain
targets, and wildcard path patterns (e.g. /.well-known/*)
- Path prefix rewrite that prepends a segment to every request before proxying
(e.g. /recipes → upstream sees /recipes/original/path)
Config is stored in the existing meta JSON column (no schema migration). Includes
integration tests for meta serialization and E2E functional tests against a real
Caddy instance covering relative/absolute destinations, all 3xx status codes, and
various wildcard combinations. Adds traefik/whoami to the test stack to verify
rewritten paths actually reach the upstream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- accept wildcard proxy host domains like *.example.com with validation and normalization
- make exact hosts win over overlapping wildcards in generated routes and TLS policies
- add unit coverage for host-pattern priority and wildcard domain handling
- add a single test:all entry point and clean up lint/typecheck issues so the suite runs cleanly
- run mobile layout Playwright checks under both chromium and mobile-iphone
When allowWebsocket=true and WAF is enabled, the WAF handler sits first
in the handler chain and processes the initial HTTP upgrade request
(GET + Upgrade: websocket). If any rule matches, Coraza can block the
handshake before SecAuditEngine captures it — producing no log entry
and an unexplained connection failure from the client's perspective.
Fix: when allowWebsocket=true, prepend a phase:1 SecLang rule that
matches Upgrade: websocket (case-insensitive) and turns the rule engine
off for that transaction via ctl:ruleEngine=off. After the 101
Switching Protocols response the connection becomes a raw WebSocket
tunnel that the WAF cannot inspect anyway, so this bypass has no impact
on normal HTTP traffic through the same host.
The rule is inserted before OWASP CRS includes so it always fires first
regardless of which ruleset is loaded.
Add 9 unit tests in caddy-waf.test.ts covering: bypass present/absent,
phase:1 placement, case-insensitive regex, nolog/noauditlog flags,
ordering before CRS, and compatibility with custom directives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug: when a proxy host had per-host WAF explicitly disabled (enabled:false)
with waf_mode:"merge" (or no waf_mode set), resolveEffectiveWaf entered the
merge branch and returned enabled:true unconditionally, applying the global
WAF to a host the user had opted out of.
Fix: add `if (host.enabled === false) return null` at the top of the merge
branch. Explicit opt-out now takes precedence over the global setting
regardless of mode. The override mode already handled this correctly.
Also extract resolveEffectiveWaf from caddy.ts into caddy-waf.ts so it
can be unit tested. Add 12 new tests covering no-config fallback,
merge opt-out regression, merge settings combination, and override mode.
What runs without OWASP CRS: only SecRuleEngine + audit directives +
any custom_directives. The @coraza.conf-recommended and CRS includes
are gated behind load_owasp_crs (fixed in previous commit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The WAF handler always prepended 'Include @coraza.conf-recommended' to the
SecLang directives regardless of load_owasp_crs. The @-prefixed paths only
resolve from the embedded coraza-coreruleset filesystem, which the Caddy
WAF plugin mounts only when load_owasp_crs=true. Without it Caddy fails:
"failed to readfile: open @coraza.conf-recommended: no such file or directory"
Fix: gate all @-prefixed Include directives behind load_owasp_crs.
Also extract buildWafHandler from caddy.ts into caddy-waf.ts so it can be
unit tested in isolation, and add tests/unit/caddy-waf.test.ts (19 tests)
covering the regression, CRS include ordering, excluded rule IDs, and
handler structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract pemToBase64Der and buildClientAuthentication from caddy.ts into
a new caddy-mtls.ts module, adding groupMtlsDomainsByCaSet to group mTLS
domains by their CA fingerprint before building TLS connection policies.
Previously all mTLS domains sharing a cert type (auto-managed, imported,
or managed) were grouped into a single policy, causing CA union: a client
cert from CA_B could authenticate against a host that only trusted CA_A.
The fix creates one policy per unique CA set, ensuring strict per-host
CA isolation across all three TLS policy code paths.
Also adds:
- tests/unit/caddy-mtls.test.ts (26 tests) covering pemToBase64Der,
buildClientAuthentication, groupMtlsDomainsByCaSet, and cross-CA
isolation regression tests
- tests/unit/instance-sync-env.test.ts (33 tests) for the five pure
env-reading functions in instance-sync.ts
- tests/integration/instance-sync.test.ts (16 tests) for
buildSyncPayload and applySyncPayload using an in-memory SQLite db
- Fix tests/helpers/db.ts to use a relative import for db/schema so it
works inside vi.mock factory dynamic imports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IssuedCertsPanel preview: only show active (non-revoked) certs
- ManageIssuedClientCertsDialog: filter out revoked by default; show
"Show revoked (N)" toggle when revoked certs exist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Split ACME / Imported / CA-mTLS into tabs with count badges
- Add clickable status summary bar (expired / expiring soon / healthy)
- Per-tab search filter by name and domain
- Replace accordion cards with DataTable for imported certs
- Slide-in Drawers (480 px) for add/edit imported and CA certs
- File upload + show/hide toggle for private key in ImportCertDrawer
- CaCertDrawer: Generate / Import PEM tabs for add, simple form for edit
- CA tab: expandable rows showing issued client certs inline
- RelativeTime component: "in 45 days" / "EXPIRED 3 days ago" with date tooltip
- Remove CreateCaCertDialog and EditCaCertDialog (replaced by CaCertDrawer)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When all issued certs for a CA are revoked, buildAuth returns null.
Previously the code would merge mTLS domains back into a policy with no
client_authentication, silently dropping the requirement and allowing
unauthenticated access (open bypass).
Fix by always splitting mTLS and non-mTLS domains first, then using
drop: true when buildAuth returns null — so a fully-revoked CA causes
Caddy to drop TLS connections for those domains rather than admit them
without a client certificate.
Also removed the redundant first buildAuth(domains) call in the
auto-managed path that was used only as an existence check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Caddy's trusted_leaf_certs is an additional check on top of CA chain
validation, not a replacement. Without trusted_ca_certs, Go's TLS
rejects the client cert before the leaf check runs, causing 'unknown ca'.
Updated buildClientAuthentication to always include the CA cert in
trusted_ca_certs for chain validation, and additionally set
trusted_leaf_certs for managed CAs to enforce revocation. When all
issued certs for a CA are revoked, the CA is excluded from
trusted_ca_certs entirely so chain validation fails for any cert from it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs fixed:
1. buildClientAuthentication was placing issued leaf cert PEMs into
trusted_ca_certs. Caddy uses that field for CA chain validation, not
leaf pinning — putting leaf certs there made chain verification fail
for every presented client cert, causing the browser to be asked
repeatedly. Fixed by using trusted_leaf_certs for managed CAs.
2. If all issued certs for a CA were revoked, the active cert map would
be empty and the code fell back to trusting the CA cert directly,
effectively un-revoking everything. Fixed by tracking which CAs have
ever had issued certs (including revoked) and keeping them in
trusted_leaf_certs mode permanently (empty list = no one trusted).
Also fix CA certificate delete action not surfacing the error message
to the user in production (Next.js strips thrown error messages in
server actions). Changed to return { success, error } and updated the
client dialog to check the result instead of using try/catch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old DB records may still have mode='DetectionOnly'. The previous
value?.mode ?? 'inherit' would pass that string into state, leaving no
engine mode button selected. Explicitly accept only 'Off'/'On'; anything
else (including legacy DetectionOnly) falls back to 'inherit'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WafSettings.mode is now 'Off' | 'On' so the legacy DB coercion guard
triggered a TS2367 type error. DB values are already normalised upstream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DetectionOnly was fundamentally broken in coraza-caddy (actually blocks
requests via anomaly scoring), caused massive audit log flooding, and the
threshold workaround had several issues:
- t:none is meaningless in a SecAction (no target to transform)
- SecRuleEngine directive ordering relative to SecAction is implementation-
defined, making the override fragile
- host.mode ?? 'DetectionOnly' fallbacks silently gave any host without an
explicit mode the broken DetectionOnly behaviour
Changes:
- Remove DetectionOnly from UI (global settings radio, per-host engine mode)
- Coerce legacy DB values of 'DetectionOnly' to 'On' in buildWafHandler
- Fix fallback defaults: host.mode ?? 'DetectionOnly' → host.mode ?? 'On'
- Fix action parsers: unknown mode defaults to 'On' (was 'DetectionOnly')
- Fix global settings defaultValue: ?? 'DetectionOnly' → ?? 'On' (or 'Off')
- Remove the fragile threshold SecAction workaround
- Update types: mode is now 'Off' | 'On' throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SecAuditEngine On logs every request through the WAF regardless of whether
any rules matched, causing massive disk I/O on busy hosts (e.g. during
Docker image pushes). RelevantOnly still captures DetectionOnly hits because
OWASP CRS rules include auditlog in their SecDefaultAction, so rule-matched
transactions are marked for audit logging. Only truly clean requests (no
rule match at all) are silently skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DetectionOnly mode: add SecAction to set anomaly score thresholds to
9999999 so rule 949110/980130 never fires; works around coraza-caddy
bug where is_interrupted=true still causes a 403 in detection mode
- Switch SecAuditEngine back to On (from RelevantOnly) so DetectionOnly
hits are captured, now safe because body parts are excluded
- SecAuditLogParts: ABIJDEFHZ → ABFHZ, dropping request body (I),
multipart files (J), intermediate response headers (D), and response
body (E) — prevents multi-MB payloads being written to audit log
- Parser: store both blocked and detected events; filter on rule matched
OR is_interrupted instead of is_interrupted only
- Add blocked column to waf_events (migration 0014); existing rows
default to blocked=true
- WAF Events UI: Blocked/Detected chip in table and drawer header
- Fix misleading help text that said to use Detection Only to observe
traffic before blocking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace file-based cert loading with inline content to fix cross-container
filesystem issues (web and caddy containers don't share the data volume):
- Imported server certs: switch from tls.certificates.load_files to
tls.certificates.load_pem (inline PEM content in JSON config)
- Client CA certs: use trusted_ca_certs (base64 DER) instead of
trusted_ca_certs_pem_files
- Fix pre-existing bug where certificates[] was placed inside
tls_connection_policies (invalid Caddy JSON field)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New `ca_certificates` table for reusable CA certs (migration 0011)
- CA cert CRUD model, server actions, and UI dialogs
- Proxy host create/edit dialogs include mTLS toggle + CA cert selection
- Caddy config generates `client_authentication` TLS policy blocks with
`require_and_verify` mode for hosts with mTLS enabled
- CA certs sync to slave instances via instance-sync payload
- Certificates page shows CA Certificates section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds checkSameOrigin() helper in auth.ts that validates the Origin header
against the Host header. If Origin is present and mismatched, returns 403.
Applied to all 5 custom POST routes flagged in CPM-003 (NEXT-CSRF-001):
- change-password, link-oauth-start, unlink-oauth, update-avatar, logout
SameSite=Lax (NextAuth default) already blocks standard cross-site CSRF;
this adds defense-in-depth against subdomain and misconfiguration scenarios.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both "waf" and "geoblock" settings were missing from the sync payload,
meaning slaves used their own (potentially unconfigured) values.
Per-host WAF was already synced via the proxyHosts table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add getTopWafRulesWithHosts() and getWafEventCountries() model queries
- WAF stats API now returns topRules with per-host breakdown and byCountry
- Analytics: replace WAF rules table with bar chart + host chip details
- Analytics: add WAF column (amber) to Top Countries table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WafEvent model: expose rawData field from DB
- DataTable: add optional onRowClick prop with hover cursor
- WafEventsClient: clicking a row opens a right-side drawer showing
all event fields plus the raw Coraza audit JSON (pretty-printed)
Safety: rawData is rendered via JSON.stringify into a <pre> element,
never via dangerouslySetInnerHTML, so attack payloads are displayed
as inert text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>