Zero-config visibility
With the defaults, audit entries already appear inline in the Connector's runtime logs via the slog mirror. docker logs and native cloud log aggregation pick them up with no extra wiring.
Connector operations
Every qURL Connector emits a structured audit record for each control-plane decision — bootstrap, knock, login, proxy, and teardown. It is on by default and already visible in your runtime logs; this guide shows how to route it to a compliance-grade file, what each event means, and how to verify the stream.
Each bootstrap, knock, login, proxy, and teardown produces one entry.
JSONL to a rotated file, and a mirror on the runtime log stream at INFO.
Ship the file for forensics, or consume the mirror from central logging.
Overview
Audit logging ships enabled in current qURL Connector releases (v0.4.0 and later). Each entry is dual-sunk: a forensic-grade JSONL file with bounded rotation, and a mirror onto the runtime log stream at INFO so central shippers — journald, CloudWatch, GCP Cloud Logging — see the same events without a bind mount.
With the defaults, audit entries already appear inline in the Connector's runtime logs via the slog mirror. docker logs and native cloud log aggregation pick them up with no extra wiring.
For a clean JSONL stream suited to forensic replay or retention, bind-mount the audit file out of the container, or point QURL_AUDIT_FILE at a host path you ship. The file is the canonical record regardless of the mirror.
Event taxonomy
A healthy tunnel session produces roughly 5 to 10 entries. The event field uses these stable strings. The proxy path emits only tunnel.proxy.allow — there is no per-proxy deny or error event today.
tunnel.bootstrap.successThe Connector registered its persistent key with the NHP server on first start or after state recovery.
tunnel.bootstrap.denyThe NHP server refused registration (bad public key, registry full, or an unauthorized bootstrap key).
tunnel.bootstrap.errorA transport, timeout, or parse error stopped the Connector from reaching the NHP server.
tunnel.knock.successThe NHP knock to the access controller succeeded and an access token was returned.
tunnel.knock.denyThe access controller accepted the transaction but issued no token for the requested resource.
tunnel.knock.errorA transport, timeout, or crypto error interrupted the knock.
tunnel.login.successThe control channel to the qURL tunnel server came up.
tunnel.login.denyThe tunnel server rejected the knock token.
tunnel.login.errorA dial, timeout, or login error occurred before a session was established.
tunnel.proxy.allowRoutes were registered with the tunnel server — emitted once per healthy cycle alongside login success.
tunnel.teardownThe tunnel session ended; the outcome field records whether the shutdown was clean or errored.
Each line is one JSON object. The values below are illustrative; your records carry their own IDs, addresses, and timestamps. Readers must tolerate unknown fields — the schema can add fields over time.
{"ts":"2026-06-12T14:03:21.512Z","event":"tunnel.knock.success","outcome":"success","actor":"prod-dashboard","trace_id":"7c1f9a2e","resource_id":"res_8f2a31","source_ip":"203.0.113.42","latency_ms":48.6,"machine_id":"e3b0c44298fc","proxy_version":"v0.4.0"}
{"ts":"2026-06-12T14:03:21.998Z","event":"tunnel.login.success","outcome":"success","actor":"prod-dashboard","trace_id":"7c1f9a2e","machine_id":"e3b0c44298fc","proxy_version":"v0.4.0"}
{"ts":"2026-06-12T15:11:09.204Z","event":"tunnel.knock.error","outcome":"error","reason":"dial_timeout","actor":"prod-dashboard","trace_id":"9a4d20b1","error":"dial tcp 198.51.100.7:62201: i/o timeout","machine_id":"e3b0c44298fc","proxy_version":"v0.4.0"}tsEvent timestamp, UTC, RFC 3339. Stamped when the event is recorded, not when it is flushed.
eventThe taxonomy string from the table above, for example tunnel.knock.success.
outcomesuccess, allow, deny, or error. This is the field to key dashboards and alerts on, not the log level.
reasonShort cause tag on deny and error entries, for example dial_timeout or knock_expired. Empty on success.
actorThe qURL Connector identity that performed the operation (its connector ID).
trace_idCorrelates knock, login, proxy, and teardown entries within one supervisor cycle.
resource_idThe qURL resource the decision is about, when applicable.
source_ipOrigin of the operation, best-effort; may be empty when the local socket address is unknown.
latency_ms, bytes_sent, bytes_receivedPer-operation metrics, present where the call site measures them.
errorUnderlying error message on the error path. Empty on policy deny and on success.
machine_id, proxy_versionHost machine identifier and the Connector build version that emitted the entry.
Configuration
The defaults are production-safe: enabled, mirrored, written to /var/log/layerv/qurl-connector/audit.log, with size, age, and count rotation. Set only the knobs you need to change in qurl-proxy.yaml.
audit:
enabled: true # default true; QURL_AUDIT_ENABLED is the env kill switch
file_path: /var/log/layerv/qurl-connector/audit.log # forensic-grade JSONL record
mirror_slog: true # also emit each entry to the runtime log stream at INFO
buffer_size: 4096 # in-process channel buffer; rarely needs tuning
max_size_mb: 100 # rotate the active file above 100 MB
max_age_days: 90 # evict rotated backups older than 90 days
max_backups: 14 # keep at most 14 rotated files
compress: true # gzip rotated backupsQURL_AUDIT_FILE redirects the file at runtime without rewriting YAML, which is the right tool in Docker Compose and Kubernetes manifests. QURL_AUDIT_ENABLED=false disables emission entirely and overrides the YAML.
# Redirect the audit file without editing YAML (compose / k8s manifests):
QURL_AUDIT_FILE=/var/log/layerv/qurl-connector/audit.log
# Disable audit emission entirely (env kill switch; overrides the YAML):
QURL_AUDIT_ENABLED=falsemax_size_mb (default 100 MB) using size, age, and count limits.max_age_days (default 90, the SOC 2 and PCI DSS minimum hot-tier retention) are evicted.max_backups (default 14) rotated files are retained; compress: true gzips them.QURL_AUDIT_FILE — not only audit.file_path — to co-locate them with runtime events.Sink topologies
The mirror and the file are independent. The most important rule is how to filter: the mirror emits every entry at INFO — a deny is a correct policy outcome, not a malfunction.
The default, and the right choice for most installs. Rotated JSONL on disk is the forensic-grade record; your central log shipper also sees every entry on the runtime stream through the slog mirror, so no bind-mount is required just to get visibility.
Set mirror_slog: false when audit volume is high and you do not want the duplicate on the runtime stream, or when the shipper is fed directly from the bind-mounted file with -v /var/log/layerv:/var/log/layerv. The file remains the canonical record.
Uncommon. Only sensible when the local file is unavailable, for example a read-only filesystem, and central logging is the sole destination. Rotation still needs a writable parent directory, so point file_path at a writable tmpfs path in that case.
deny and error outcomes. A shipper that filters level >= WARN will see no audit records at all.outcome attribute (outcome=deny, outcome=error), never on the log level.Verify and operate
After the Connector connects, confirm audit entries are flowing on whichever sink you intend to consume, then confirm rotation is keeping the volume bounded.
# Default install: audit entries appear inline in the Connector's runtime
# logs via the slog mirror at INFO. Filter for the audit event prefix:
docker logs -f qurl-connector-prod-dashboard | grep '"event":"tunnel.'
# Compliance-grade JSONL: bind-mount the audit directory out of the
# container (docker run ... -v /var/log/layerv:/var/log/layerv ...),
# then tail the clean stream from the host:
sudo tail -f /var/log/layerv/qurl-connector/audit.log
# Confirm rotated backups are bounded and gzipped:
sudo ls -lh /var/log/layerv/qurl-connector/tunnel.knock.success and tunnel.login.success appear after the Connector connects.Troubleshooting
Most issues are filter mismatches, a redirected sink that left bootstrap events behind, or rotation tuning.
Audit entries are written per control-plane decision, so they appear after the first tunnel cycle (typically 5 to 10 per session). Confirm audit.enabled is true and that QURL_AUDIT_ENABLED is not set to false.
Every audit entry is mirrored at INFO, including deny and error outcomes — a shipper filtering level >= WARN drops all of them. Key your dashboards and alerts on the outcome attribute (outcome=deny, outcome=error), or ship the file sink directly.
The first tunnel.bootstrap.* entries are emitted before the YAML loads, so they follow QURL_AUDIT_FILE (or the default path), not audit.file_path. When redirecting the sink, set QURL_AUDIT_FILE so bootstrap and runtime events co-locate.
Lower max_size_mb, max_backups, or max_age_days. Rotated backups are gzipped when compress is true; JSONL typically compresses 5 to 10 times.
Set QURL_AUDIT_ENABLED=false. The env kill switch overrides the YAML and suppresses both the file and the slog mirror until it is removed.
Related docs
Start with the standard qURL Connector install guide, then use this guide to route and verify the audit stream for your compliance and forensics needs.