Skip to content
Hogin Hogin
Go back

Teleport instead of a VPN: audited access to servers, Kubernetes, and databases

8 мин чтения

A bastion host, static SSH keys, a separate kubeconfig per developer, and yet another password for psql — the typical production access stack, and nobody in it can quickly answer “who did what on this server at 3am on Tuesday.” A zero-trust VPN like NetBird or Cloudflare Tunnel doesn’t fix this: it gives you an encrypted network path to the perimeter, but what happens after you connect is still up to SSH keys and the auth.log on the box itself. Teleport solves a different problem — not “how do I get onto the network,” but “how do I get access to a specific resource — a pod, a database, a server — so that every action is auditable and revocable.”

Table of contents

Open Table of contents

VPN and Teleport cover different layers

NetBird and Cloudflare Tunnel operate at the network layer: they decide who can reach a private subnet or a specific host by IP and port. Inside that subnet, it’s business as usual — SSH by key, kubectl with a local kubeconfig, psql with a password from .pgpass. Zero trust at the network layer is necessary but not sufficient for privileged access: SOC2 and ISO 27001 don’t ask “who could physically reach the database” — they ask “who ran which specific commands in production, and when.” A network VPN can’t answer that question at all; it has no visibility above the transport layer.

Teleport doesn’t replace a VPN — it sits on top of whatever network you already have (including an existing NetBird or Cloudflare Tunnel setup) and grants access not to a network, but to a resource: a named server, a specific Kubernetes cluster, a specific database. The practical difference: a network VPN can’t grant “read-only access to the checkout pod in the prod namespace for 20 minutes” — Teleport can, because its RBAC is scoped to resources and roles, not CIDR blocks.

Zero-trust network vs. audited resource access — different layers, different questions

The problem with static SSH keys and kubeconfig

The classic bastion + SSH-key setup breaks down predictably at scale:

All of these are static secrets with an unbounded lifetime, and they leak the same way every time: a compromised laptop, an access grant nobody revoked after someone left, or a CI runner whose workspace ended up in the wrong hands. Teleport removes static secrets as a category, replacing them with short-lived certificates.

How short-lived certificates work

The Teleport Auth Service is an internal CA. A user logs in once via tsh login (through SSO — Okta, Google Workspace, GitHub — or through an existing SPIFFE/SPIRE deployment as the identity source) and gets back not a password but an X.509 and SSH certificate with a default TTL of 8-12 hours. From there:

When the certificate expires, access disappears on its own — no manual key revocation across dozens of hosts. A leaked certificate is bounded in both time and scope: it’s tied to the user’s roles at the moment it was issued, not to a static key that keeps working until someone remembers to delete it.

A short-lived certificate instead of a key: TTL and role binding instead of permanent access

Resource-level RBAC, not network-level

Teleport roles (teleport.yamlroles) describe access in terms of labels on resources: env: prod, app: checkout, a specific database name or namespace. That’s a fundamentally finer grain than a network ACL:

Combined with an existing identity provider — SSO, or SPIFFE/SPIRE if your cluster already runs SPIRE as a workload identity source — Teleport roles map onto SSO groups or SPIFFE IDs instead of being created by hand for every new hire. Access is granted and revoked in the same place a person is onboarded and offboarded — the HR system or identity directory — not in a separate authorized_keys list.

Session recording and incident review

Every SSH session, kubectl exec, or database query that goes through Teleport is proxied through the Auth/Proxy Service and, by default, recorded as a session recording — terminal input/output with timestamps. Reviewing an incident doesn’t require reconstructing host state from bash history (which the user could have cleared) — it just takes:

tsh recordings ls --format=json | jq '.[] | select(.user=="alice")'
tsh play <session-id>

tsh play replays the session as a terminal recording — character by character, at real speed or fast-forwarded. For a SOC2/ISO audit, that’s not “we believe access was limited” — it’s an actual record of who typed what, and when, on production.

What you need to wire up access to a test Kubernetes cluster

Minimal setup: a Teleport cluster (self-hosted teleport or managed Teleport Cloud), tbot for getting identity into CI/automation, and one Kubernetes cluster registered as a resource.

Install teleport and log in:

# on the machine running the Teleport Auth/Proxy Service
curl https://cdn.teleport.dev/install.sh | bash -s 17.0.0

# from a workstation
tsh login --proxy=teleport.example.com --user=alice

Register the Kubernetes cluster as a resource accessible through Teleport (the teleport-kube-agent runs right inside the cluster as a Helm chart):

helm repo add teleport https://charts.releases.teleport.dev
helm install teleport-kube-agent teleport/teleport-kube-agent \
  --set roles=kube \
  --set proxyAddr=teleport.example.com:443 \
  --set kubeClusterName=prod-cluster \
  --create-namespace -n teleport

A role that restricts access to read-only in one namespace (file reader.yaml, applied with tctl create -f):

kind: role
version: v7
metadata:
  name: kube-reader-checkout
spec:
  allow:
    kubernetes_labels:
      env: staging
    kubernetes_resources:
      - kind: pod
        namespace: checkout
        verbs: ["get", "list"]

Get access instead of a static kubeconfig:

tsh kube login prod-cluster
kubectl get pods -n checkout

No kubeconfig file with a permanent token ends up on disk — tsh kube login embeds a certificate scoped to the current tsh session into the local kubeconfig, and it stops working the moment the login TTL expires.

How to verify it works

Check that a kubectl exec in this cluster shows up in the session recording, not just in the Kubernetes API audit log:

kubectl exec -it -n checkout deploy/checkout -- sh
# in another terminal, with an auditor role
tsh recordings ls --format=json | jq '.[-1]'
tsh play $(tsh recordings ls --format=json | jq -r '.[-1].id')

If the session replays character by character, the proxy is actually terminating and recording each connection — not just routing traffic straight to the API server.

Bottom line

A VPN (NetBird, Cloudflare Tunnel) answers “who can reach the network” — a correct and sufficient layer for access to internal services that aren’t privileged on their own. Teleport answers a different question — “who accessed exactly which resource, with what audit trail” — and that’s the layer you need the moment a production database, a production Kubernetes cluster, or anything requiring SOC2/ISO audit is in scope. You don’t have to deploy both at once: if a network VPN already exists and privileged access isn’t yet a separate concern, it can wait. But the first time someone asks “who logged into prod last night,” the answer should be tsh play, not grepping through .bash_history.


Share this post:

Next Post
Database schema as code: Atlas on top of CloudNativePG instead of hand-rolled migrations