Skip to main content
Back to qURL Connector install guide

Connector operations

Audit logging for the qURL Connector.

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.

1Decision

Each bootstrap, knock, login, proxy, and teardown produces one entry.

2Dual sink

JSONL to a rotated file, and a mirror on the runtime log stream at INFO.

3Your pipeline

Ship the file for forensics, or consume the mirror from central logging.

Overview

Audit is on by default; the only choice is where it lands.

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.

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.

Compliance-grade record

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

One entry per control-plane decision.

A healthy tunnel session produces roughly 5 to 10 entries. The event field uses these stable strings. The proxy path emits only qurl.connector.proxy.allow — there is no per-proxy deny or error event today.

qurl.connector.bootstrap.success

The Connector registered its persistent key with the NHP server on first start or after state recovery.

qurl.connector.bootstrap.deny

The NHP server refused registration (bad public key, registry full, or an unauthorized bootstrap key).

qurl.connector.bootstrap.error

A transport, timeout, or parse error stopped the Connector from reaching the NHP server.

qurl.connector.knock.success

The NHP knock to the access controller succeeded and an access token was returned.

qurl.connector.knock.deny

The access controller accepted the transaction but issued no token for the requested resource.

qurl.connector.knock.error

A transport, timeout, or crypto error interrupted the knock.

qurl.connector.login.success

The control channel to the qURL tunnel server came up.

qurl.connector.login.deny

The tunnel server rejected the knock token.

qurl.connector.login.error

A dial, timeout, or login error occurred before a session was established.

qurl.connector.proxy.allow

Routes were registered with the tunnel server — emitted once per healthy cycle alongside login success.

qurl.connector.teardown

The tunnel session ended; the outcome field records whether the shutdown was clean or errored.

Entry schema

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.

Sample JSONL
{"ts":"2026-06-12T14:03:21.512Z","event":"qurl.connector.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":"qurl.connector.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":"qurl.connector.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"}

ts

Event timestamp, UTC, RFC 3339. Stamped when the event is recorded, not when it is flushed.

event

The taxonomy string from the table above, for example qurl.connector.knock.success.

outcome

success, allow, deny, or error. This is the field to key dashboards and alerts on, not the log level.

reason

Short cause tag on deny and error entries, for example dial_timeout or knock_expired. Empty on success.

actor

The qURL Connector identity that performed the operation (its connector ID).

trace_id

Correlates knock, login, proxy, and teardown entries within one supervisor cycle.

resource_id

The qURL resource the decision is about, when applicable.

source_ip

Origin of the operation, best-effort; may be empty when the local socket address is unknown.

latency_ms, bytes_sent, bytes_received

Per-operation metrics, present where the call site measures them.

error

Underlying error message on the error path. Empty on policy deny and on success.

machine_id, proxy_version

Host machine identifier and the Connector build version that emitted the entry.

Configuration

Leaving the block out produces a working pipeline.

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.

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 backups

Environment overrides

QURL_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.

Env
# 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=false

Retention defaults

  • The active file rotates above max_size_mb (default 100 MB) using size, age, and count limits.
  • Backups older than max_age_days (default 90, the SOC 2 and PCI DSS minimum hot-tier retention) are evicted.
  • At most max_backups (default 14) rotated files are retained; compress: true gzips them.
  • Bootstrap events fire before the YAML loads, so set QURL_AUDIT_FILE — not only audit.file_path — to co-locate them with runtime events.

Sink topologies

Pick the destination that matches your pipeline.

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.

File plus runtime-log mirror (default)

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.

File only

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.

Runtime-log mirror only

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.

Filter by attribute, not by level

  • Every audit record mirrors at INFO, including deny and error outcomes. A shipper that filters level >= WARN will see no audit records at all.
  • Key dashboards and alerting on the outcome attribute (outcome=deny, outcome=error), never on the log level.
  • If your shipper can only filter by level, route audit through the file sink and ship that file directly — the file path is canonical regardless.

Verify and operate

Confirm the stream after install.

After the Connector connects, confirm audit entries are flowing on whichever sink you intend to consume, then confirm rotation is keeping the volume bounded.

Verification
# 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":"qurl.connector.'

# 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/

Healthy when all four are true

  • qurl.connector.knock.success and qurl.connector.login.success appear after the Connector connects.
  • The chosen sink — file, runtime-log mirror, or both — carries the entries.
  • Dashboards bucket on the outcome attribute and surface deny and error counts.
  • Rotated, gzipped backups exist and stay within the configured limits.

Troubleshooting

Common audit-pipeline snags.

Most issues are filter mismatches, a redirected sink that left bootstrap events behind, or rotation tuning.

The audit file or log stream shows no audit events.

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.

The central log shipper receives no audit records even though they appear in docker logs.

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.

Bootstrap events land in a different file than the runtime events.

The first qurl.connector.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.

The audit volume is growing faster than expected.

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.

You need to silence audit during incident triage without editing YAML.

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

Install first, then wire the audit pipeline.

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.