# VPN console

> Read the VPN overview screen: live per-shard session snapshots, durable session and refusal history, the credential projection, and source-address violations.

The VPN overview screen answers one question that a tunnel operator portal
normally cannot: **who is connected right now**. It reads the engine's own
live state, not the database rows that describe what *should* be connected,
and pairs that with durable history so you can also see who *was* connected
and who was *refused*.

## What this screen shows

The screen is built from three sources, stacked top to bottom:

| Section                            | Source                                  | Procedure                 |
| ---------------------------------- | --------------------------------------- | ------------------------- |
| Totals + **Live sessions** table   | Engine per-shard session snapshots      | `adminVpn.liveSessions`   |
| **Usage by tag** panel             | Aggregated consumption for one tag key  | `adminVpn.usageByTag`, `tags.keys` |
| **Refused connections** + **Session history** | Postgres                     | `adminVpn.history`        |

Four totals sit above the live table:

- **Connected now** — number of sessions in the snapshot.
- **Received** / **Sent** — aggregate `rxBytes` / `txBytes`.
- **Spoofed packets dropped** — see
  [Source-address violations](#source-address-violations). This is given
  equal billing with throughput deliberately: it is a security number, and a
  security number nobody looks at is useless.

Each live row carries the user, the device label on the credential, the
leased VPN IP, the peer address the client is connecting from, Rx/Tx byte
counters, how long the session has been up, and the gateway shard serving it.

## Live sessions vs durable history

These two tables look similar and mean different things. Read them
differently.

**Live sessions** comes from the engine's per-shard session snapshots
(`telequick:vpn:sessions:<shard>` in Redis), which carry a TTL. It
reflects *reality* — what the gateway currently has open. The table polls
every 5 seconds, because a stale live table is worse than no live table when
you are deciding whether a revoke actually took effect. If the engine
restarts, the snapshots expire with it, so this table is not a record of
anything; it is a window.

**Session history** and **Refused connections** come from Postgres. Rows are
written by the control plane's `vpn-events-sync` consumer, reading the
engine's durable event bus — not from anything Redis carries. These rows
survive an engine restart. The history query returns the most recent 25
sessions and refusals.

So: if a session is missing from the live table but present in history with
`still open` in the **Ended** column, you are looking at a durable record
whose live counterpart has aged out of the snapshot.

Session history rows include the assigned VPN IP, connect and disconnect
timestamps, the close reason where one was recorded, Rx/Tx totals, and
whether a second factor was used for that session (**2FA** column).

## The credential projection and when to refresh access state

The gateway does not query the database on each handshake. It authenticates
against a **projection** of the active credential set that the control plane
writes for it. Every mutation that changes access — creating, editing or
revoking a credential — re-projects that set on its own, so in normal
operation you never touch this.

The **Refresh access state** button in the page header calls
`adminVpn.resync`. It exists for the one case that leaves no trace anywhere
in the console: the engine (or Redis) came up *after* the projection had
already been written, so nothing re-projected and the gateway is holding a
credential set that is empty or stale.

On success the button reports how many active credentials were refreshed
(`projected`). On failure it surfaces the error inline. Reach for it when:

- Nobody appears in the live table but you expect someone to be connected.
- A credential you just revoked still seems to be accepted.
- The engine or its Redis was restarted or replaced.

It is idempotent — it rebuilds the projection from the current active
credential set, so running it when nothing is wrong changes nothing.

## Fail-closed behaviour when access state is unavailable

If the gateway cannot read the active credential set, it **fails closed**: it
refuses new connections rather than admitting them unauthenticated. This is
why an empty **Live sessions** table has two very different explanations —
genuinely nobody is connected, or the gateway is refusing everyone because it
has no access state to check against.

The empty state on the live table says as much. When you hit it and expected
traffic, check gateway health and then
[refresh the access state](#the-credential-projection-and-when-to-refresh-access-state).

## Refusal reasons and close reasons

**Refused connections** records attempts the gateway rejected. Each row has
the time, the *claimed* user (the identity the client asserted — which is why
the column is labelled "claimed" and may be the raw claim rather than a
resolved user), the device label on the credential if one matched, the
refusal reason, and the peer address.

Reasons fall into two classes, and the console colours them differently
because they are two different conversations:

- **Authentication failures**, highlighted in red: `bad_auth` (an unknown,
  invalid or revoked token) and any `mfa_*` reason (a missing, invalid or
  replayed second-factor code). A cluster of these from one peer address is a
  security event. A revoked credential trying to reconnect lands here too.
- **Everything else**, shown unhighlighted — for example the address pool
  being exhausted at the time of the attempt. These are capacity and
  configuration problems, not intrusion signals.

Close reasons are recorded separately, on completed sessions. When a session
has a close reason, **Session history** shows it in parentheses after the
disconnect timestamp; sessions still open show `still open`.

## Source-address violations

The **Spoofed packets dropped** total counts packets a client sent from an
address it was **not leased**. The engine enforces the source address on the
tunnel and drops anything that does not match, so the traffic never reaches
the network — the counter is the only place you see it happened.

When the counter is non-zero the screen raises a banner. Treat a non-zero
value as a security signal, not a performance one. It is benign only if a
device is genuinely misconfigured; otherwise it means something is attempting
to impersonate another client on the VPN. Cross-reference the peer addresses
in the live table and in **Refused connections** to find the device.

## Attribute consumption with tags

The **Usage by tag** panel breaks tunnel consumption down by the values of a
single tag key. The key picker is populated by `tags.keys`, which lists the
tag keys in use in your org ordered by how many resources carry them, so the
most-used key is preselected.

Two things to know about reading it:

- The panel reports **consumption, not spend**. VPN is bundled rather than
  rated per unit, so there is no per-unit price to total up.
- Users **inherit tags from their network**. Tagging a network therefore
  allocates every user on it in one move, which is normally the cheapest way
  to get a complete breakdown.

The panel counts **users** as its resource noun.

## Enabling session and refusal recording

Durable history is a deployment-level capability. `adminVpn.history` returns
a `recording` flag alongside the rows, and when it is false the console shows
a prominent warning above both history tables:

> History is not being recorded. Session and refusal history is unavailable
> on this deployment.

Read that banner literally. When recording is off, **empty history tables
mean nothing** — they are empty because nothing is being written, not because
nothing has happened. The live table and the totals are unaffected; they come
from the engine, not from Postgres.

If you need refusal history for security review or session history for
audit, recording has to be enabled on the deployment before the events occur.
It is not backfilled — the `vpn-events-sync` consumer writes rows as events
arrive on the bus.

## Related

- [Authentication](/concepts/authentication)
- [Telemetry](/platform/telemetry)
