Zero Trust Everything: The Complete Guide to Self-Hosting Teleport for Secure Infrastructure Access

CoderOasis runs Teleport internally for everything — admin panel access, secure database connections, Kubernetes, and zero-trust philosophy across our entire team. This is the complete technical guide to installing, configuring, and running your own self-hosted Teleport cluster from scratch.

Let me tell you exactly how someone gets access to CoderOasis's servers.

They don't get a password. They don't get an SSH key. There's no VPN to connect to. Nobody emails them a credential. The first time they need access to anything — a server, a database, the Ghost CMS admin panel — they open a browser, authenticate through our identity provider with hardware MFA enforced, and Teleport issues them a short-lived cryptographic certificate that expires in eight hours. That certificate is scoped precisely to the resources their role permits. When the eight hours are up, it's gone. There's nothing to revoke because there's nothing persistent to begin with.

Every session is logged. Every command typed in an SSH session is recorded. Every database query runs through an audit trail. Every access attempt — successful or not — lands in a structured event log that we can search, replay, and alert on. If someone on our team's laptop is compromised, the attacker has a stale certificate that expired hours ago and no persistent credentials to pivot with.

This is Teleport. And I want to be specific about what it replaced at CoderOasis: a shared bastion server with SSH keys distributed to team members, a LastPass shared folder for database passwords, and a VPN that half our team forgot to connect to before doing things they shouldn't have done without it. The migration to Teleport took a weekend. The security posture improvement was immediate and measurable.

This article is the complete technical guide to self-hosting Teleport. Not the 15-minute quickstart that gets you a demo cluster with no production hardening. The real setup — architecture decisions, TLS configuration, RBAC design, database access, Kubernetes integration, MCP server protection, application proxying, machine identity for CI/CD, and the audit log pipeline. Everything.

If you want the cloud-hosted version and don't want to run your own infrastructure, Teleport Cloud is the path. This guide is for teams that either need data sovereignty, have a compliance requirement for on-prem, or -- like us -- just prefer to own their infrastructure stack. We covered that philosophy across the self-hosted email guide and the full productivity stack guide. Teleport fits the same philosophy: critical security infrastructure should be something you own and understand, not something you rent and trust blindly.

What Is Teleport and Why Is It Different From a VPN or Bastion Host?

Before the install commands, the mental model. You need to understand what Teleport actually is architecturally, because "it's like a VPN" and "it's like a bastion host" are both wrong in ways that matter.

The Problem With VPNs

A VPN grants broad network access. Once you're on the VPN, you're on the network. You can reach any host on that network segment that the firewall allows. This is the antithesis of zero trust. "Being on the network" is not an identity. It's a location claim that attackers routinely fake, steal, or route through compromised machines.

VPNs also don't tell you what happened. You know someone connected. You don't know which specific database they queried, which commands they ran on which server, or whether they accessed anything they shouldn't have. VPN logs show connection events, not access events.

The Problem With SSH Key Management

SSH keys are credentials. Credentials can be stolen. Credentials can be accidentally committed to Git repositories -- we've all seen the GitHub secret scanner alerts. SSH keys don't expire. A key generated for a contractor who left six months ago is still valid on every server where their authorized_keys entry was added -- if anyone remembered to remove it, which they often don't.

At CoderOasis, we had a legacy bastion server with authorized_keys files. When we audited it before migrating to Teleport, we found four keys for people who no longer had any relationship with the project. Those keys had root access. Nobody had thought to remove them because there was no offboarding process that included "remove SSH keys from bastion."

What Teleport Actually Is

Teleport is an identity-aware access proxy. Every piece of it is built around the principle that identity is the perimeter. Not the network. Not the firewall. The cryptographically verified identity of the specific person or machine making a specific request.

Architecturally, Teleport has three core services:

The Auth Service is the certificate authority and the brain of the cluster. It issues short-lived X.509 and SSH certificates to verified users and machines. It stores the audit log. It enforces RBAC. It maintains the cluster's certificate authority. In small deployments, this runs on the same host as the Proxy Service.

The Proxy Service is the public-facing entry point. Users and agents connect to it. It handles TLS termination, routes connections to the appropriate resources, and streams sessions to the Auth Service for audit logging. This is the only thing that needs to be publicly accessible.

Agents are lightweight processes that run on or near the resources you want to protect. The SSH agent opens a reverse tunnel to the Proxy Service -- your servers never need to be publicly accessible. The Database agent proxies connections to your databases. The Kubernetes agent sits in front of your cluster. The Application agent proxies internal web apps.

The reverse tunnel architecture is one of Teleport's most underrated features. Your database doesn't need to be reachable from the internet. Your Kubernetes API server doesn't need to be publicly accessible. The agent initiates an outbound connection to the Proxy Service, and traffic flows through that tunnel. Your firewall never needs an inbound rule for production databases.

The Zero Trust Properties Teleport Actually Provides

No standing privileges. By default, nobody has persistent access to anything. Access is granted by issuing a certificate for the requested resource. When the certificate expires, access ends automatically. No manual revocation required.

No shared secrets. There are no passwords, no SSH keys, no API tokens for accessing infrastructure. Everything is certificate-based. A compromised developer laptop doesn't give an attacker persistent access -- only access until the current certificate expires (maximum TTL: whatever you configure, typically 8-24 hours).

Complete audit trail. Every access event -- login, command, query, keystroke in a recorded session -- is a structured event with the authenticated user's identity, the resource accessed, the timestamp, and the session ID. Not "someone connected from this IP at this time." Specifically: which user, which certificate, which resource, and for SSH sessions, every command they typed.

Least privilege by default. RBAC roles define exactly which resources each user can access and what they can do with them. A developer role can SSH into dev servers but not production. A database analyst role can SELECT from the reporting database but not UPDATE or access the payments database. The Kubernetes role can kubectl get pods in the application namespace but can't touch kube-system. These constraints are enforced at the proxy level – they're not just audit controls, they're access controls.

Architecture Decisions Before You Install Anything

Single Node vs. High Availability

For small teams and self-hosted setups, a single node running both Auth and Proxy services is perfectly viable. That's what CoderOasis runs. The trade-off is availability: if the Teleport node goes down, access to your infrastructure goes through the CLI (SSH keys you've kept as emergency access) or your emergency runbook.

For production systems where Teleport downtime means engineering teams can't access anything, HA is worth the complexity:

  • Auth Service: Run multiple instances behind a load balancer. Requires external state storage -- etcd cluster or DynamoDB for the backend, not the default local filesystem.
  • Proxy Service: Stateless, run multiple instances behind a load balancer.
  • Session recording storage: S3 (or MinIO for self-hosted) instead of local disk.
    For most self-hosted teams, start single-node. Migrate to HA when you have the team to operate it.

Storage Backend

The default backend is a local SQLite-compatible database at /var/lib/teleport. Fine for single-node. For HA or for teams that want better durability guarantees, the options are:

  • DynamoDB (AWS) -- The native HA backend if you're on AWS. Works out of the box.
  • etcd -- For on-prem HA. Requires running an etcd cluster.
  • Firestore (GCP) -- GCP native equivalent.
  • CockroachDB -- Community-contributed, works well for self-hosted HA.

Where to Run It

Teleport's Proxy Service needs to be publicly accessible (or at least reachable by your team) on port 443. Everything else can be private. Our setup: a Hetzner VPS with Teleport Proxy+Auth, every other server and service behind private networking with only the Teleport agent making outbound connections to the proxy.

Requirements for the proxy/auth node:

  • 2 vCPU minimum, 4 recommended
  • 4GB RAM minimum for single-node
  • 50GB SSD for audit logs (size depends on session recording volume)
  • Static public IP or hostname
  • Port 443 open for web UI and TLS multiplexing
  • Ubuntu 24.04 LTS or Debian 12 recommended

TLS Certificate Strategy

Teleport needs a valid TLS certificate. Three options:

  1. ACME/Let's Encrypt (recommended): Teleport handles certificate renewal automatically. Requires port 80 or port 443 to be accessible.
  2. Existing certificate: Supply cert-file and key-file pointing to your certificate files. Manage renewal yourself.
  3. Self-signed (development only): Do not use in production. Browser and client warnings at every connection.
    We use ACME. Teleport handles the renewal. It's never failed in two years.

Installing Teleport

Preparation

On your Proxy/Auth host:

# Update the system
apt update && apt upgrade -y
 
# Create the teleport user -- we run the service as non-root for everything
# except the initial binding on port 443
useradd --system --no-create-home --shell /bin/false teleport
 
# Set up the firewall
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp        comment "SSH -- replace with Teleport SSH after setup"
ufw allow 443/tcp       comment "Teleport Web UI and TLS multiplexing"
ufw enable
 
# DNS: set up your domain before proceeding
# You need:
# teleport.yourdomain.com -> your server IP (A record)
# *.teleport.yourdomain.com -> same IP (wildcard for app subdomains)

Installing the Teleport Binary

# Install the Teleport package -- always pin to a specific major version
# As of this writing, v17 is current stable
TELEPORT_VERSION="17.5.1"
curl https://cdn.teleport.dev/install.sh | bash -s ${TELEPORT_VERSION}
 
# Verify installation
teleport version
tctl version
tsh version
 
# All three should show the same version

Generating the Initial Configuration

# Generate base configuration with ACME/Let's Encrypt
# Replace teleport.yourdomain.com with your actual domain
# Replace [email protected] with your actual email
 
sudo teleport configure -o file \
  --acme \
  [email protected] \
  --cluster-name=teleport.yourdomain.com
 
# This creates /etc/teleport.yaml
cat /etc/teleport.yaml

Editing the Configuration File

The generated config is a starting point. Here's the full production configuration we recommend, with every setting explained:

# /etc/teleport.yaml
version: v3
 
teleport:
  nodename: teleport-main
  data_dir: /var/lib/teleport
  log:
    output: /var/log/teleport/teleport.log
    severity: INFO
    format:
      output: json        # JSON for log aggregation (Grafana, ELK, etc.)
  # Limit connections per source IP to prevent abuse
  connection_limits:
    max_connections: 5000
    max_users: 250
 
auth_service:
  enabled: true
  cluster_name: "teleport.yourdomain.com"
  listen_addr: "127.0.0.1:3025"   # Auth only needs to be localhost if proxy is colocated
  public_addr: "teleport.yourdomain.com:3025"
 
  # Authentication configuration
  authentication:
    type: local                   # 'local' for local users + optional SSO
    second_factor: "webauthn"     # Enforce hardware MFA -- no TOTP-only
    webauthn:
      rp_id: "teleport.yourdomain.com"
 
    # Enforce per-session MFA for privileged access
    # Users must tap their hardware key for every new SSH/DB session
    require_session_mfa: false     # Set to true for highest security
 
  # Session recording -- record every session to local storage
  # For production: use S3/MinIO for durability
  session_recording: "node"        # Record at node, not proxy
 
  # Certificate lifetimes
  # User certs: short -- they re-authenticate often
  # Host certs: longer -- servers don't log in daily
  client_idle_timeout: 1h          # Disconnect idle sessions after 1 hour
  disconnect_expired_cert: true    # Hard disconnect when cert expires
 
  # Audit log
  audit_events_uri:
    - "file:///var/lib/teleport/log"
 
proxy_service:
  enabled: true
 
  # Single port for everything -- multiplexed TLS
  # This handles web UI, SSH, DB, Kubernetes, app traffic on 443
  web_listen_addr: "0.0.0.0:443"
  public_addr: "teleport.yourdomain.com:443"
 
  # ACME certificate management
  acme:
    enabled: true
    email: "[email protected]"
 
  # Kubernetes proxy address -- reachable by kubectl via Teleport
  kube_public_addr: "teleport.yourdomain.com:443"
 
  # Multiplexing mode -- puts everything on port 443
  # No need to expose 3022, 3023, 3024 separately
  proxy_listener_mode: multiplex
 
  # PROXY protocol -- enable if behind a load balancer that sends PROXY headers
  # Leave unset (or 'off') for direct Teleport deployments
  # proxy_protocol: "off"
 
ssh_service:
  enabled: true         # Enable SSH on the Teleport node itself
  listen_addr: "0.0.0.0:3022"
  labels:
    role: teleport-proxy
    env: production
 
# These are disabled here -- run as separate agents on protected resources
# (shown in subsequent sections)
app_service:
  enabled: false
 
db_service:
  enabled: false
 
kubernetes_service:
  enabled: false

Systemd Service and Starting Teleport

# Create log directory
mkdir -p /var/log/teleport
chown teleport:teleport /var/log/teleport
 
# Install as systemd service
teleport install systemd -o /etc/systemd/system/teleport.service
 
# Enable and start
systemctl daemon-reload
systemctl enable teleport
systemctl start teleport
 
# Watch the startup logs -- verify everything initialized
journalctl -u teleport -f

You should see lines like:

{"level":"info","msg":"Auth service is starting on 127.0.0.1:3025"}
{"level":"info","msg":"Proxy service is starting on 0.0.0.0:443"}
{"level":"info","msg":"SSH service is starting on 0.0.0.0:3022"}
{"level":"info","msg":"Certificate issued from Let's Encrypt"}

Navigate to https://teleport.yourdomain.com in a browser. You should see the Teleport login page with a valid TLS certificate.

Creating the First Admin User

# Create the initial admin user
# --roles: editor (can modify resources) + access (can access infrastructure)
# --logins: the Linux usernames this user can log in as via SSH
 
tctl users add admin \
  --roles=editor,access \
  --logins=ubuntu,root
 
# This prints a signup URL:
# https://teleport.yourdomain.com/web/invite/<token>
# Open this in a browser to complete registration and enroll MFA

Complete MFA enrollment -- Teleport supports FIDO2/WebAuthn hardware keys (YubiKey, etc.), biometric authenticators (Touch ID, Windows Hello), and TOTP apps. We enforce hardware keys for admin accounts.

After registration, test access:

# Install tsh locally on your workstation
# macOS
brew install teleport
 
# Linux
curl https://cdn.teleport.dev/install.sh | bash
 
# Log in
tsh login --proxy=teleport.yourdomain.com --user=admin
 
# List accessible servers
tsh ls
 
# Should show the Teleport proxy node itself (since we enabled SSH service)

Designing Your RBAC Model

Before adding any resources, design your role structure. RBAC is where Teleport earns its keep and where teams that rush through setup pay for it later.

Teleport's Role Model

A Teleport role has four sections:

  • allow: What this role permits
  • deny: What this role explicitly blocks (deny beats allow)
  • options: Behavior modifiers (session TTL, MFA requirements, recording settings)
  • metadata: Name, version, labels
    Labels are the key mechanism. Nodes, databases, apps, and Kubernetes clusters all have labels. Roles grant access by matching labels -- not by listing specific resources. This is how you maintain "all production servers" access without updating roles every time you add a server.

The Role Structure We Use at CoderOasis

# /etc/teleport/roles/contributor.yaml
# For external contributors and limited team members
kind: role
version: v7
metadata:
  name: contributor
  description: "Read-only access to non-sensitive resources"
spec:
  allow:
    # Can SSH to development servers only
    node_labels:
      env: ["dev", "staging"]
    logins:
      - developer
      - "{{internal.logins}}"   # Maps to their Linux username
 
    # Can access non-production databases
    db_labels:
      env: ["dev", "staging"]
    db_names:
      - "appdb_dev"
      - "appdb_staging"
    db_users:
      - "readonly"
 
    # Can view (not replay) their own sessions
    rules:
      - resources: ["session"]
        verbs: ["list", "read"]
 
  deny:
    # Explicitly block production even if labels match
    node_labels:
      env: ["production"]
    db_labels:
      env: ["production"]
 
  options:
    max_session_ttl: "8h"
    disconnect_expired_cert: true
    # Require MFA per session for database access
    require_session_mfa: "hardware_key"  # Require hardware key tap per session
    # Record all sessions
    record_session:
      desktop: true
      default: "best_effort"
 
---
# /etc/teleport/roles/engineer.yaml
# Full-time team members -- access to all environments with audit
kind: role
version: v7
metadata:
  name: engineer
  description: "Full engineer access -- all environments"
spec:
  allow:
    # All servers
    node_labels:
      "*": "*"
    logins:
      - ubuntu
      - "{{internal.logins}}"
 
    # All databases -- read/write
    db_labels:
      "*": "*"
    db_names:
      - "*"
    db_users:
      - "appuser"
      - "readonly"
      - "{{internal.db_users}}"
 
    # All apps
    app_labels:
      "*": "*"
 
    # Kubernetes -- developer groups on all clusters
    kubernetes_groups:
      - developers
      - staging-admins
    kubernetes_labels:
      "*": "*"
    kubernetes_resources:
      - kind: "*"
        namespace: "*"
        name: "*"
 
    rules:
      - resources: ["session"]
        verbs: ["list", "read"]
 
  options:
    max_session_ttl: "12h"
    disconnect_expired_cert: true
    require_session_mfa: "hardware_key"
    record_session:
      default: "best_effort"
 
---
# /etc/teleport/roles/admin.yaml
# CoderOasis core team -- full access including Teleport management
kind: role
version: v7
metadata:
  name: coderoasis-admin
  description: "Full administrative access including Teleport cluster management"
spec:
  allow:
    node_labels:
      "*": "*"
    logins:
      - root
      - ubuntu
      - "{{internal.logins}}"
 
    db_labels:
      "*": "*"
    db_names:
      - "*"
    db_users:
      - "*"
 
    app_labels:
      "*": "*"
 
    kubernetes_groups:
      - system:masters
    kubernetes_labels:
      "*": "*"
    kubernetes_resources:
      - kind: "*"
        namespace: "*"
        name: "*"
 
    # Full Teleport cluster management
    rules:
      - resources: ["*"]
        verbs: ["*"]
 
  options:
    max_session_ttl: "8h"
    disconnect_expired_cert: true
    require_session_mfa: "hardware_key"
    record_session:
      default: "best_effort"
 
---
# /etc/teleport/roles/access-request-approver.yaml
# Can approve just-in-time access requests
kind: role
version: v7
metadata:
  name: access-approver
spec:
  allow:
    review_requests:
      roles:
        - engineer
        - coderoasis-admin
      preview_as_roles:
        - engineer

Apply roles with tctl:

# Apply all role files
for f in /etc/teleport/roles/*.yaml; do
  tctl create -f "$f"
  echo "Applied $f"
done
 
# Verify roles exist
tctl get roles

Adding SSH Nodes

Every server you want to access through Teleport gets a Teleport SSH agent. The agent runs as a systemd service, opens a reverse tunnel to your Proxy Service, and registers the node in the cluster.

Install and Configure a Node Agent

On the target server:

# Install Teleport (same version as the cluster)
TELEPORT_VERSION="17.5.1"
curl https://cdn.teleport.dev/install.sh | bash -s ${TELEPORT_VERSION}
 
# Generate a join token on the Auth server
# Tokens are one-time-use and expire -- this prevents unauthorized nodes from joining
# Run this on your Auth/Proxy host:
# tctl tokens add --type=node --ttl=1h
# Copy the token output

On the Auth host, generate the token:

tctl tokens add --type=node --ttl=1h
# Output:
# The invite token: abc123def456...
# This token will expire in 60 minutes

Back on the target server, create the config:

# /etc/teleport.yaml on the target node
version: v3
 
teleport:
  nodename: web-server-01                    # Unique name for this node
  data_dir: /var/lib/teleport
  log:
    output: /var/log/teleport/teleport.log
    severity: INFO
 
  # Connect via the Proxy Service -- this works from behind NAT
  proxy_server: "teleport.yourdomain.com:443"
 
  # The join token
  auth_token: "abc123def456..."
 
  # CA pin -- prevents connecting to a different cluster if DNS is poisoned
  # Get this from: tctl status (on the auth server)
  ca_pin: "sha256:abc123..."
 
auth_service:
  enabled: false
 
proxy_service:
  enabled: false
 
ssh_service:
  enabled: true
  listen_addr: "0.0.0.0:3022"
 
  # Labels -- these are what RBAC roles match against
  labels:
    env: production
    role: webserver
    team: platform
    region: eu-west
 
  # Enhanced session recording -- capture commands at kernel level
  # Requires Linux 5.8+ with BPF capabilities
  enhanced_recording:
    enabled: true
    command_buffer_size: 8192
    disk_buffer_size: 300
    network_buffer_size: 8192
 
app_service:
  enabled: false
 
db_service:
  enabled: false
 
kubernetes_service:
  enabled: false
# Install and start the service
teleport install systemd -o /etc/systemd/system/teleport.service
systemctl daemon-reload
systemctl enable teleport
systemctl start teleport
 
# Verify the node appears in the cluster (run on Auth host)
tctl nodes ls
# Output:
# Node Name          Address              Labels
# ────────────────── ──────────────────── ───────────────────────────
# web-server-01      127.0.0.1:3022       env=production,role=webserver

Accessing Nodes

From your workstation:

# List all nodes you can access
tsh ls
 
# Filter by label
tsh ls env=production
 
# SSH into a node
tsh ssh ubuntu@web-server-01
 
# Or use the standard SSH syntax through the Teleport proxy
ssh -o ProxyCommand="tsh proxy ssh %r@%h:%p" ubuntu@web-server-01
 
# Set up SSH config for native SSH client support
tsh config > ~/.ssh/config.d/teleport.conf
# Now: ssh ubuntu@web-server-01 works natively

The session is authenticated, authorized against your RBAC role, and recorded. Run tsh recordings ls to see your sessions. The Web UI at https://teleport.yourdomain.com lets you replay any recorded session.

Hardening: Close Port 22

Once Teleport SSH is working, close port 22 on your nodes. All access goes through Teleport. Port 22 is eliminated as an attack surface:

# On each node after verifying Teleport SSH works
ufw delete allow 22/tcp
ufw reload
 
# Keep emergency access: a local console or cloud provider serial console
# in case Teleport is unavailable -- but remove the public SSH attack surface

Database Access

This is one of Teleport's most powerful and most underused features. Instead of distributing database passwords to every developer who needs occasional database access, Teleport issues short-lived database certificates and proxies the connection. No developer ever sees a database password.

How Database Access Works

The Teleport Database Service runs as an agent near your database. When a developer runs tsh db connect, Teleport:

  1. Verifies their certificate and RBAC permissions
  2. Issues a short-lived X.509 certificate for the specific database and user
  3. Routes their connection through the database agent
  4. Logs every query (for supported databases) to the audit log
    The database never receives the developer's identity directly -- it receives a certificate from Teleport's CA, mapped to a database user your RBAC configuration specifies. The database sees a connection from readonly or appuser -- not from a named individual. Teleport's audit log maps that connection to the specific engineer who initiated it.

Setting Up PostgreSQL Access

On the database host (or a host that can reach the database):

# /etc/teleport.yaml -- database agent configuration
version: v3
 
teleport:
  nodename: db-agent-01
  data_dir: /var/lib/teleport
  proxy_server: "teleport.yourdomain.com:443"
  auth_token: "db-join-token-here"   # tctl tokens add --type=db --ttl=1h
  ca_pin: "sha256:..."
 
auth_service:
  enabled: false
 
proxy_service:
  enabled: false
 
ssh_service:
  enabled: false
 
db_service:
  enabled: true
 
  # List of databases to protect
  databases:
    - name: "coderoasis-production"
      description: "Production PostgreSQL"
      protocol: "postgres"
      uri: "localhost:5432"           # Or the database host if remote
      labels:
        env: production
        type: postgres
        team: platform
      tls:
        mode: "verify-full"           # Verify PostgreSQL's certificate
        # server_name: "postgres.internal"
 
    - name: "coderoasis-staging"
      description: "Staging PostgreSQL"
      protocol: "postgres"
      uri: "pg-staging.internal:5432"
      labels:
        env: staging
        type: postgres
 
    - name: "coderoasis-mysql"
      description: "Production MySQL -- legacy"
      protocol: "mysql"
      uri: "localhost:3306"
      labels:
        env: production
        type: mysql
 
app_service:
  enabled: false
 
kubernetes_service:
  enabled: false

Configure PostgreSQL to trust Teleport certificates:

-- In PostgreSQL, create the database users that Teleport will map to
-- Teleport issues certificates for these users -- no passwords
CREATE USER "readonly" WITH LOGIN;
GRANT CONNECT ON DATABASE appdb TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
 
CREATE USER "appuser" WITH LOGIN;
GRANT CONNECT ON DATABASE appdb TO appuser;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;

In pg_hba.conf, add certificate authentication for the Teleport CA's client certificates:

# Allow Teleport certificate authentication from the db agent
hostssl  appdb  readonly   127.0.0.1/32   cert  clientcert=verify-ca
hostssl  appdb  appuser    127.0.0.1/32   cert  clientcert=verify-ca

Configure PostgreSQL to trust Teleport's certificate authority:

# Get Teleport's database CA certificate
# Run on the auth host:
tctl auth export --type=db-client > /etc/postgresql/teleport-ca.crt
 
# In postgresql.conf:
# ssl = on
# ssl_ca_file = '/etc/postgresql/teleport-ca.crt'

Start the database agent and connect:

# On the auth host
tctl tokens add --type=db --ttl=1h
# Copy the token, update auth_token in /etc/teleport.yaml on db agent host
 
# On the db agent host
systemctl start teleport
 
# Verify database appears in cluster
# On auth host:
tctl db ls
 
# From developer workstation:
tsh db ls
 
# Log in to the database
tsh db login --db-user=readonly --db-name=appdb coderoasis-production
 
# Connect using your native client -- no password prompt
psql "service=coderoasis-production"
 
# Or directly:
tsh db connect coderoasis-production --db-user=readonly --db-name=appdb

Every query runs through Teleport's audit log. From the web UI, you can see the full query history for any database session: who connected, when, which queries they ran.

Database Access for MySQL, MongoDB, Redis

The configuration is nearly identical -- just change protocol:

databases:
  - name: "mongo-production"
    protocol: "mongodb"
    uri: "mongodb://mongo.internal:27017"
    labels:
      env: production
 
  - name: "redis-cache"
    protocol: "redis"
    uri: "redis://redis.internal:6379"
    labels:
      env: production

Connect:

tsh db connect mongo-production --db-user=appuser
# Opens a mongo shell authenticated via Teleport certificate
 
tsh db connect redis-cache
# Opens a redis-cli session

Kubernetes Access

Teleport's Kubernetes integration is where the zero-trust model really starts to shine for modern infrastructure. Instead of distributing kubeconfig files that contain long-lived credentials, every kubectl call flows through Teleport's proxy, authenticated by the user's current certificate.

Registering a Kubernetes Cluster

In the Kubernetes cluster you want to protect, install the Teleport Kubernetes agent via Helm:

# Add the Teleport Helm repository
helm repo add teleport https://charts.releases.teleport.dev
helm repo update
 
# Generate a join token for the Kubernetes agent
# Run on auth host:
tctl tokens add --type=kube --ttl=1h
# Copy the token
 
# Create namespace
kubectl create namespace teleport-agent
 
# Create the values file
cat > teleport-kube-agent-values.yaml <<EOF
proxyAddr: "teleport.yourdomain.com:443"
authToken: "kube-join-token-here"    # From tctl tokens add above
kubeClusterName: "production-k8s"   # Name in Teleport -- can differ from actual name
 
# Labels for RBAC matching
labels:
  env: production
  region: eu-west
  team: platform
 
# Resource limits
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi
EOF
 
# Install the agent
helm install teleport-kube-agent teleport/teleport-kube-agent \
  --namespace teleport-agent \
  --values teleport-kube-agent-values.yaml
 
# Verify the agent connected
kubectl get pods -n teleport-agent
# Wait for: teleport-kube-agent-... Running
 
# On auth host -- verify cluster appears
tctl kube ls

Kubernetes RBAC Configuration

Teleport maps users to Kubernetes RBAC groups. The Teleport role specifies which Kubernetes groups a user gets when they access a cluster. You define the Kubernetes RBAC permissions for those groups in the cluster itself.

Teleport role defining Kubernetes access:

# The 'engineer' role from earlier already includes:
# kubernetes_groups: [developers, staging-admins]
# This maps the Teleport user to these Kubernetes RBAC groups

Kubernetes RBAC resources for those groups:

# Apply this in your Kubernetes cluster
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: developers
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create"]        # Allow kubectl exec -- Teleport records these sessions
 
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: developers
subjects:
  - kind: Group
    name: developers           # Matches kubernetes_groups in Teleport role
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: developers
  apiGroup: rbac.authorization.k8s.io

Using Kubernetes through Teleport:

# From developer workstation -- after tsh login:
 
# List available Kubernetes clusters
tsh kube ls
 
# Authenticate to a cluster and update kubeconfig
tsh kube login production-k8s
 
# Now kubectl works through Teleport
kubectl get pods -n application
kubectl logs -n application deployment/backend
kubectl exec -it -n application pod/backend-xxxx -- /bin/sh
 
# Every kubectl call is logged:
# - Who made the request (Teleport user identity)
# - Which cluster
# - Which namespace and resource
# - The response code
# - Timestamp
 
# Switch between clusters
tsh kube login staging-k8s
kubectl get pods      # Now talking to staging

Every kubectl exec session is recorded in Teleport. You can replay it from the web UI. This is the audit trail that compliance requires and that most Kubernetes setups simply don't have.

Proxying Internal Web Apps

At CoderOasis we use Teleport's Application Service to proxy our Ghost CMS admin panel and our internal monitoring dashboards. Instead of exposing these on the public internet or requiring a VPN, Teleport proxies them as subdomains of our Teleport cluster domain. Only authenticated, authorized users can reach them.

How Application Access Works

The App agent registers an internal application with the Teleport cluster. Teleport creates a subdomain: ghost-admin.teleport.yourdomain.com. When an authenticated user navigates to that URL, Teleport verifies their certificate and role, then proxies the request to the internal application. The internal app never needs to be publicly accessible.

This is how our Ghost admin panel works. The admin panel runs on localhost:2368 on our Ghost server. Teleport proxies it at ghost.teleport.yourdomain.com. Nobody outside our team can reach it, and access is RBAC-controlled.

Configuring Application Access

On the server running the application (or a server that can reach the app):

# /etc/teleport.yaml -- app agent configuration
version: v3
 
teleport:
  nodename: app-agent-01
  data_dir: /var/lib/teleport
  proxy_server: "teleport.yourdomain.com:443"
  auth_token: "app-join-token"
  ca_pin: "sha256:..."
 
auth_service:
  enabled: false
 
proxy_service:
  enabled: false
 
ssh_service:
  enabled: false
 
app_service:
  enabled: true
 
  apps:
    # Ghost CMS Admin Panel
    - name: "ghost-admin"
      description: "CoderOasis Ghost CMS Admin"
      uri: "http://localhost:2368"         # Internal URL of the app
      public_addr: "ghost.teleport.yourdomain.com"  # Public URL via Teleport
      labels:
        env: production
        app: ghost
        team: editorial
 
    # Grafana dashboard -- internal monitoring
    - name: "grafana"
      description: "Infrastructure Monitoring"
      uri: "http://localhost:3000"
      public_addr: "grafana.teleport.yourdomain.com"
      labels:
        env: production
        app: grafana
        team: platform
 
    # Traefik dashboard -- internal, never publicly exposed
    - name: "traefik"
      description: "Traefik Reverse Proxy Dashboard"
      uri: "http://localhost:8080"
      public_addr: "traefik.teleport.yourdomain.com"
      labels:
        env: production
        app: traefik
        team: platform
 
    # Any internal web service
    - name: "internal-api-docs"
      description: "Internal API Documentation"
      uri: "http://api-server.internal:8000/docs"
      public_addr: "api-docs.teleport.yourdomain.com"
      labels:
        env: production
        team: engineering
 
db_service:
  enabled: false
 
kubernetes_service:
  enabled: false

Add wildcard DNS:

# In your DNS configuration:
# *.teleport.yourdomain.com -> your Teleport proxy IP
# This allows Teleport to serve apps on subdomains automatically

Update RBAC to allow app access:

The roles we defined earlier already include app_labels: "*": "*" for engineers and admins. For more granular control:

# Restrict Ghost admin access to editorial role only
kind: role
version: v7
metadata:
  name: editorial
spec:
  allow:
    app_labels:
      app: ["ghost"]
    # No SSH, no database access
  options:
    max_session_ttl: "8h"

Access the apps:

# List available applications
tsh apps ls
 
# Log in to an application -- opens browser to the app
tsh apps login ghost-admin
# Browser opens ghost.teleport.yourdomain.com -- authenticated via Teleport
 
# Or browse directly:
# Navigate to ghost.teleport.yourdomain.com
# Teleport intercepts, checks your certificate, proxies to localhost:2368
# Ghost admin panel appears -- no Ghost-level authentication needed
# (Teleport auth replaces it)

The Web UI equivalent: in the Teleport web interface, your authorized applications appear as tiles. Click one, it opens in a new tab, authenticated. No passwords, no separate logins.

Zero Trust for AI Agents

This is the most forward-looking feature in Teleport's 2025-2026 release cycle and the reason we're covering it specifically. If you're running AI coding assistants, automation agents, or MCP servers that give AI agents access to your infrastructure, you have an identity problem that most people haven't solved yet.

The problem: your AI agent has a token or API key to access your MCP server. That token has whatever permissions were granted when someone created it. It doesn't expire unless you rotate it manually. If the agent runtime or the token is compromised, an attacker has standing access to everything the agent could access.

Teleport's Secure MCP capability applies the same certificate-based, RBAC-enforced, fully-audited model to MCP connections that it applies to SSH and databases.

How MCP Access Works

Teleport proxies connections between MCP clients (AI agents like Claude, Cursor, or your custom tooling) and MCP servers (the backend services that give AI agents tools). The agent authenticates to Teleport, receives a short-lived certificate, and the MCP connection is brokered through Teleport's policy engine. Every tool call is logged.

# /etc/teleport.yaml -- MCP-capable agent configuration
version: v3
 
teleport:
  nodename: mcp-agent-01
  data_dir: /var/lib/teleport
  proxy_server: "teleport.yourdomain.com:443"
  auth_token: "mcp-join-token"
 
auth_service:
  enabled: false
 
proxy_service:
  enabled: false
 
ssh_service:
  enabled: false
 
# Register MCP servers as applications
app_service:
  enabled: true
 
  apps:
    - name: "coderoasis-mcp"
      description: "CoderOasis Internal MCP Server"
      uri: "http://localhost:8811"      # Your MCP server address
      protocol: "mcp"                   # Tells Teleport this is MCP traffic
      labels:
        env: production
        type: mcp
        team: engineering

RBAC for MCP access:

kind: role
version: v7
metadata:
  name: ai-agent-reader
  description: "Read-only MCP access for AI coding assistants"
spec:
  allow:
    app_labels:
      type: ["mcp"]
      env: ["dev", "staging"]
    # Restrict which MCP tools the AI can call
    # This is policy-as-code for your AI agents
  options:
    max_session_ttl: "1h"     # Short TTL for AI agent sessions
    require_session_mfa: false # AI agents can't tap hardware keys

Connect an MCP client through Teleport:

# Get the MCP server connection config for your AI client
tsh apps login coderoasis-mcp
 
# For Claude Desktop, Cursor, or other MCP clients:
# Configure the MCP server URL as the Teleport-proxied address
# The client gets a short-lived certificate for each session
# All tool calls are audited in the Teleport audit log

This means every time your AI coding assistant calls a tool – queries a database, reads a file, executes code – that call is in Teleport's audit log with the agent's identity. Not an anonymous API key. A specific identity, with role-based permissions, that you can revoke or restrict at any time.

GitHub / Git Repository Access

Teleport's Git integration proxies access to GitHub and other Git providers through identity-aware SSH. Every git push, git clone, or git pull is logged with the authenticated user's Teleport identity.

# Register GitHub as an application through Teleport
app_service:
  apps:
    - name: "github"
      description: "GitHub access via Teleport"
      uri: "https://github.com"
      protocol: "tcp"
      public_addr: "github.teleport.yourdomain.com"
      labels:
        type: git

More practically, Teleport issues SSH certificates for Git operations. Configure your Git client:

# Configure Git to use Teleport's SSH proxy
# After tsh login, tsh provides a local SSH agent with short-lived GitHub certs
tsh git clone https://github.com/yourorg/yourrepo.git
 
# Or configure SSH for GitHub to route through Teleport
# ~/.ssh/config:
# Host github.com
#   IdentityAgent /Users/you/.tsh/sockets/agent.12345

Machine Identity: CI/CD Pipeline Access

Production CI/CD pipelines need to deploy code, run migrations, push container images, and access production resources. The traditional approach is to create service account credentials and store them as CI/CD secrets. Those secrets are long-lived, have broad permissions, and are stored in systems you don't fully control.

Teleport Machine ID (tbot) gives your CI/CD pipeline a cryptographic identity and issues short-lived certificates for each job run.

Setting Up tbot for GitHub Actions

On a server that your GitHub Actions runner can reach, or in the runner itself:

# Install tbot (part of the Teleport binary)
# In your CI/CD environment -- GitHub Actions example
 
# Create a bot user in Teleport
tctl bots add github-actions \
  --roles=ci-deployer \
  --token-ttl=1h
 
# Output includes a join token and bot name

Define the CI deployer role:

kind: role
version: v7
metadata:
  name: ci-deployer
spec:
  allow:
    # SSH access to production -- for deployment scripts
    node_labels:
      env: production
      purpose: deploy
    logins:
      - deploy
 
    # Database access -- for running migrations
    db_labels:
      env: production
    db_names:
      - "appdb"
    db_users:
      - "migrator"
 
    # Kubernetes -- for rolling deployments
    kubernetes_groups:
      - ci-deployer
    kubernetes_labels:
      env: production
    kubernetes_resources:
      - kind: deployment
        namespace: "application"
        name: "*"
 
  options:
    max_session_ttl: "1h"     # Short TTL appropriate for CI jobs
    disconnect_expired_cert: true

GitHub Actions workflow:

# .github/workflows/deploy.yml
name: Deploy to Production
 
on:
  push:
    branches: [main]
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write     # Required for OIDC token
      contents: read
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Install Teleport
        uses: teleport-actions/setup@v1
        with:
          version: '17'
 
      - name: Authenticate to Teleport
        uses: teleport-actions/auth@v2
        with:
          proxy: teleport.yourdomain.com:443
          token: ${{ secrets.TELEPORT_BOT_TOKEN }}
          # Teleport issues a short-lived certificate for this job run
 
      - name: Run Database Migrations
        run: |
          tsh db login --db-user=migrator --db-name=appdb appdb-production
          tsh db connect appdb-production -- psql -f migrations/latest.sql
 
      - name: Deploy to Kubernetes
        run: |
          tsh kube login production-k8s
          kubectl set image deployment/backend \
            backend=registry.yourdomain.com/backend:${{ github.sha }} \
            -n application
 
      - name: Verify Deployment
        run: |
          kubectl rollout status deployment/backend -n application

The CI pipeline has no stored database passwords or Kubernetes credentials. Each job gets a fresh certificate that expires when the job completes. Every action is logged in Teleport's audit trail under the bot identity.

Just-in-Time Access Requests

Not everyone needs production database access all the time. Teleport's access request system lets users request elevated access that requires approval, with automatic expiry.

Configuring Access Requests

Update the engineer role to allow requesting admin-level access:

# Add to the engineer role's spec.allow section:
request:
  roles:
    - coderoasis-admin
    - db-admin
  # Maximum duration for the elevated access
  max_duration: "4h"
  # Require reason for the request
  annotations:
    reason: "required"

Add an approver role:

kind: role
version: v7
metadata:
  name: access-approver
spec:
  allow:
    review_requests:
      roles:
        - coderoasis-admin
        - db-admin
      # Can see what the requester would be able to do with the elevated role
      preview_as_roles:
        - coderoasis-admin

Requesting and approving access:

# Developer requests temporary admin access
tsh request create \
  --roles=db-admin \
  --reason="Need to investigate production query performance -- Incident #1234" \
  --max-duration=2h
 
# Request created: request-id-abc123
# Waiting for approval...
 
# On Slack/Teams: Teleport sends notification to approvers
# Approvers can also use the web UI
 
# Approver reviews and approves
tctl request approve request-id-abc123 \
  --reason="Approved for incident investigation"
 
# Developer's CLI gets the approval notification
# Certificate is reissued with the additional role
# 2 hours later: certificate expires, elevated access ends automatically

Integrate with Slack for approval notifications:

# Teleport access request plugin for Slack
# Deploy as a separate process or Docker container
 
# /etc/teleport-slack-plugin/teleport-slack.toml
[teleport]
addr = "teleport.yourdomain.com:443"
identity = "/var/lib/teleport-slack/identity"
 
[slack]
token = "xoxb-slack-bot-token"
 
[role_to_recipients]
# Access requests for admin role go to the #ops channel
"coderoasis-admin" = ["#ops", "[email protected]"]
"db-admin" = ["#ops"]

Configuring SSO Integration

Teleport local users work fine for small teams. For larger teams with an existing identity provider, SSO integration is the right move. Teleport supports GitHub, Okta, Azure AD, Google Workspace, and any SAML 2.0 provider.

GitHub SSO (What We Use at CoderOasis)

# GitHub OAuth connector
# Create in /etc/teleport/connectors/github.yaml
 
kind: github
version: v3
metadata:
  name: github
spec:
  # Create a GitHub OAuth App at github.com/settings/developers
  client_id: "your-github-oauth-app-client-id"
  client_secret: "your-github-oauth-app-client-secret"
  redirect_url: "https://teleport.yourdomain.com/v1/webapi/github/callback"
 
  # Map GitHub teams to Teleport roles
  teams_to_roles:
    # GitHub org/team -> Teleport role mapping
    - organization: "coderoasis"
      team: "admins"
      roles: ["coderoasis-admin", "access-approver"]
 
    - organization: "coderoasis"
      team: "engineers"
      roles: ["engineer"]
 
    - organization: "coderoasis"
      team: "contributors"
      roles: ["contributor"]
# Apply the connector
tctl create -f /etc/teleport/connectors/github.yaml
 
# Test it -- navigate to the web UI and click "Sign in with GitHub"
# GitHub OAuth flow completes, Teleport maps teams to roles, certificate issued

With SSO configured, offboarding is immediate: remove someone from the GitHub organization, their next login fails, their current certificates expire within hours. No SSH keys to revoke, no database passwords to rotate, no VPN accounts to disable.

Audit Log and Session Recording

Everything Teleport logs goes to the audit log. Configuring where and how that log is stored is a production concern.

Default: Local Files

The default stores audit events as NDJSON files in /var/lib/teleport/log/. Works for getting started. Not suitable for long-term storage on a single node.

Production: Structured Logging to External Systems

auth_service:
  audit_events_uri:
    - "file:///var/lib/teleport/log"   # Local backup
    - "dynamodb://teleport-audit"      # DynamoDB for HA/AWS
    # OR
    # - "firestore://teleport-audit"   # GCP Firestore
 
  # Session recordings (the actual terminal content)
  # For production: store in S3/MinIO, not local disk
  audit_sessions_uri: "s3://your-s3-bucket/teleport-sessions?region=eu-west-1"
  # OR for self-hosted S3-compatible (MinIO):
  # audit_sessions_uri: "s3://teleport-sessions?endpoint=https://minio.internal:9000&region=us-east-1"

Querying the Audit Log

# Search audit events
tctl events search --query='event="session.start"' --from=2026-05-01 --to=2026-05-02
 
# Find all database sessions by a specific user
tctl events search \
  --query='event="db.session.query" AND user="alice"' \
  --from=2026-05-01
 
# Find all failed access attempts
tctl events search --query='event="auth" AND success=false'
 
# Export for SIEM ingestion (outputs JSON)
tctl events search --query='event!=""' --format=json | \
  curl -X POST https://your-siem.internal/api/events \
    -H "Content-Type: application/json" \
    -d @-

Session Replay

# List recorded sessions
tsh recordings ls
 
# Replay a specific session in the terminal
tsh play <session-id>
 
# Export session recording as video (useful for incident reports)
tsh play --format=pty <session-id> > session.cast
# Convert with asciinema for sharing
 
# Web UI: Sessions -> select any session -> Play
# Shows exact terminal output, keystroke by keystroke

Teleport Connect: The Desktop Application

For teams less comfortable with the CLI, Teleport Connect is a desktop application (macOS, Windows, Linux) that provides a GUI for everything tsh does:

  • Browse your servers, databases, Kubernetes clusters, and applications
  • One-click SSH sessions that open in integrated tabs
  • Database connections that open in your preferred GUI tool
  • Kubernetes context switching
    Install:
# macOS
brew install --cask teleport-connect
 
# Linux -- download from goteleport.com/download
# Direct link to AppImage, .deb, or .rpm
 
# Windows -- MSI installer from goteleport.com/download

For non-engineers on your team who need occasional access to internal tools, Teleport Connect is a dramatically lower barrier than the CLI.

Monitoring and Alerting

Health Checks

# Check cluster status
tctl status
 
# Output:
# Cluster  teleport.yourdomain.com
# Version  17.5.1
# CA pin   sha256:abc123...
# Auth     Connected
# Proxy    Running
 
# Health endpoint for load balancers / uptime monitoring
curl https://teleport.yourdomain.com/healthz
# Returns 200 OK when healthy
 
# Prometheus metrics endpoint (configure in teleport.yaml)
# diag_addr: "127.0.0.1:3000"
# Then scrape: http://127.0.0.1:3000/metrics

Prometheus + Grafana Integration

# Add to /etc/teleport.yaml under the teleport: section
teleport:
  diag_addr: "127.0.0.1:3000"   # Expose metrics locally

Key metrics to alert on:

  • teleport_audit_failed_disk_monitoring_total -- audit log disk issues
  • teleport_connected_resources -- number of agents connected
  • teleport_auth_attempts -- authentication attempts (alert on spikes)
  • process_resident_memory_bytes -- memory usage of the Teleport process
  • teleport_proxy_peer_client_dial_error_total -- proxy connectivity errors

Log-Based Alerting

The JSON audit log makes it trivial to set up alerts via your log aggregation tool:

# Grafana Loki alert for failed root SSH attempts
{app="teleport"} | json | event="session.start" | login="root" | success="false"
# Alert on any user accessing production databases outside business hours
{app="teleport"} |
  json |
  event="db.session.start" |
  db_labels_env="production" |
  line_format "{{.user}} {{.db_name}} {{.time}}"

Troubleshooting Common Issues

Node Won't Join the Cluster

# Check the node can reach the proxy
curl -k https://teleport.yourdomain.com/healthz
nc -zv teleport.yourdomain.com 443
 
# Verify the join token is valid and not expired
tctl tokens ls    # Run on auth host
 
# Check the CA pin matches
tctl status       # Run on auth host -- shows current CA pin
 
# Check node logs for specific error
journalctl -u teleport -n 100 --no-pager | grep -i error

Certificate Issues

# Check certificate expiry
tsh status        # Shows current certificate and expiry
 
# Force re-login
tsh logout && tsh login --proxy=teleport.yourdomain.com --user=admin
 
# Check ACME certificate renewal
journalctl -u teleport | grep -i acme
 
# Manually trigger ACME renewal (if cert is near expiry)
systemctl restart teleport

Database Access Fails

# Verify the database agent is running
tctl db ls        # Should show the database
tsh db ls         # User-visible list
 
# Check database agent logs
journalctl -u teleport -n 50 | grep "db_service"
 
# Verify PostgreSQL trusts Teleport CA
# In pg_hba.conf: confirm hostssl entries exist
# In postgresql.conf: confirm ssl_ca_file points to teleport-ca.crt
 
# Check Teleport can reach the database
tctl db test-connection coderoasis-production --db-user=readonly

Kubernetes Access Fails

# Verify Kubernetes agent is connected
tctl kube ls
 
# Check if your role grants the right Kubernetes groups
tsh status      # Shows current role assignments and kubernetes_groups
 
# Verify Kubernetes RBAC resources exist
kubectl get clusterrolebinding developers -o yaml
 
# Re-authenticate to Kubernetes
tsh kube login production-k8s --all     # Forces fresh cert
 
# Check what kubectl context is active
kubectl config current-context

Upgrading Teleport

Teleport releases major versions roughly every three months. The upgrade process is rolling -- agents can be one major version behind the Proxy/Auth.

# Check current versions
teleport version
tctl version
 
# Upgrade Auth/Proxy first (always upgrade Auth before Proxy, Proxy before agents)
# Using apt (Debian/Ubuntu):
apt update && apt install teleport
 
# Restart the service
systemctl restart teleport
 
# Verify new version
teleport version
 
# Upgrade agents (can lag one major version behind cluster)
# SSH to each node and run the same apt upgrade + restart

For automated upgrades, Teleport provides an Automatic Updater:

# Enable automatic patch-version upgrades (stays within major version)
teleport-update enable --proxy=teleport.yourdomain.com

The CoderOasis Setup in Production

Here's exactly what our deployment looks like:

Infrastructure:

  • One Hetzner CX32 VPS (4 vCPU, 8GB RAM, 80GB SSD) running Auth+Proxy
  • SSH agents on every production server (3 servers)
  • Database agent proxying PostgreSQL and MySQL
  • App agents for Ghost admin, Grafana, and Traefik dashboard
  • Kubernetes agent on our Docker Swarm equivalent (we run Portainer + Docker, not Kubernetes -- but the principle is identical)
    Authentication:
  • GitHub SSO with team-to-role mapping
  • Hardware MFA enforced for all users (YubiKey 5)
  • Session MFA required for database access
    Roles:
  • coderoasis-admin -- Traven and two trusted team leads
  • engineer -- Active contributors
  • contributor -- External writers who need Ghost admin access only
  • ci-deployer -- GitHub Actions bot identity
    What replaced:
  • SSH key bastion host -- gone
  • Shared database passwords in LastPass -- gone
  • VPN for internal access -- gone
  • Manual access revocation on offboarding -- gone (GitHub team removal handles it)
    Audit:
  • All events logged to local files + shipped to Grafana Loki
  • Session recordings stored on the Teleport node (we don't do enough sessions to justify S3)
  • Alerts configured for any production access between midnight and 6 AM
    The total cost: approximately €12/month for the Hetzner VPS. Compared to Teleport Cloud at roughly $10/user/month for a small team, self-hosting pays for itself at 2+ users and gives us full data sovereignty for the audit logs and session recordings.

Community Edition vs. Enterprise

Teleport Community Edition (AGPL-3.0) includes:

  • SSH, database, Kubernetes, and application access
  • RBAC with role-based access control
  • Session recording and audit logging
  • GitHub, GitLab, OIDC, and SAML SSO
  • Machine identity (tbot)
  • Access requests with approvals
  • Web UI and tsh CLI
  • MCP server access (added in 2025)
    Teleport Enterprise adds:
  • FedRAMP/FIPS 140-2 compliance mode
  • Hardware Security Module (HSM) support
  • Advanced policy controls (ABAC, geographic restrictions)
  • SOC2/ISO27001 compliance features
  • Dedicated support SLA
  • Teleport Identity Security (access graph visualization)
  • Active Directory / Windows Desktop access with full session recording
    For 95% of self-hosted use cases, Community Edition is sufficient. That's what CoderOasis runs. If you're in a regulated industry with specific compliance certifications (FedRAMP, HIPAA, PCI-DSS), Enterprise is worth evaluating.

Frequently Asked Questions

How many users can a single-node Teleport cluster handle?

Comfortably: hundreds. The Teleport Auth Service handles auth events efficiently, and the Proxy Service is designed for high concurrency. A single node with 4 vCPU and 8GB RAM can handle thousands of connected agents and hundreds of concurrent user sessions without difficulty. CoderOasis runs a 4 vCPU / 8GB node with zero performance concerns.

What happens when the Teleport node is down?

Users lose access through Teleport. Existing SSH sessions remain active (they don't drop when Auth goes down). New sessions can't be established. This is why we maintain emergency SSH access via cloud provider serial console and a small break-glass process documented in our runbook. For teams that can't tolerate Teleport downtime, HA deployment is the answer.

Can Teleport replace our VPN entirely?

For accessing internal resources: yes, completely. For network-level access patterns (one service reaching another service on the same network), Teleport isn't the right tool -- you want service mesh, firewall rules, or VPC peering for that. Teleport replaces VPNs for human access to infrastructure, not for service-to-service traffic.

Does Teleport work with Windows servers?

Yes, with caveats. SSH access to Windows is limited. Teleport's Windows Desktop Access uses RDP proxied through Teleport with full session recording. It requires Active Directory integration and a Teleport Windows Desktop Service. Community Edition does not include Windows Desktop Access -- that requires Enterprise. For Linux-first teams, this isn't a concern.

How do we handle emergency access if Teleport is unavailable?

Document a break-glass procedure:

  1. All team leads have a physical YubiKey with an emergency SSH key
  2. That SSH key is stored in an encrypted vault (we use Vaultwarden -- see the self-hosted stack guide)
  3. Each production server has a single emergency authorized_keys entry for that key
  4. Using the emergency key triggers an alert (via SSH command logging to a separate syslog server)
  5. Any use of the emergency key requires a post-incident report
    The emergency key has never been used. That's the point -- Teleport's availability is high enough that we haven't needed it. But it exists, and everyone knows where it is.

This is the access model we trust for CoderOasis, and it's the model I'd recommend to any team that takes security seriously without wanting to make life miserable for the people actually doing the work. Zero trust doesn't have to mean zero usability. Teleport proves that -- our team reaches servers, databases, and internal tools faster through Teleport than they did through the old VPN and SSH key system, because everything is in one place, everything is discoverable, and nothing requires a manual onboarding ticket to set up.

For sysadmin and security topics, check out the SysAdmin topic section and the Cybersecurity coverage on CoderOasis. The CVE-2026-31431 article on the kernel privilege escalation we covered is directly relevant context for why removing SSH key management from the equation matters – you can't have your Linux keys stolen if they don't exist.