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.
The problem with static SSH keys and kubeconfig
The classic bastion + SSH-key setup breaks down predictably at scale:
- A key has no TTL. A compromised
id_rsakeeps working until it’s manually removed from everyauthorized_keyson every host — a manual step that gets skipped on a machine or two in the chaos of an incident. - kubeconfig is a flat bearer token or client cert with no built-in audit trail. Anyone holding the file can run
kubectl execinto any pod the token has rights to, and the cluster logs will only show the ServiceAccount name or the certificate’s CN — not the person’s name. - Database access is its own mess. The production Postgres password lives in a password manager, a secrets store, or straight in
.pgpass, and rotating it is the offboarding checklist item everyone forgets.
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:
- SSH — Teleport signs both the host certificate and the user certificate;
sshtrusts the Teleport CA instead of a pile of individual public keys scattered across every machine. - Kubernetes — instead of a static kubeconfig,
tsh kube login <cluster>generates a kubeconfig with a client certificate tied to the user’s identity, valid only until the session expires. - Databases — the Teleport Database Service proxies the connection to Postgres/MySQL/MongoDB, swapping the password for an mTLS certificate; neither the human nor the application ever needs to know the real database password.
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.
Resource-level RBAC, not network-level
Teleport roles (teleport.yaml → roles) 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:
- a network ACL can allow or block access to port 6443 (the Kubernetes API) as a whole;
- a Teleport role can allow
kubectl get podsin thestagingnamespace while fully blockingexecand the entireprodnamespace — over the exact same network path.
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.