Most infrastructure access is held together with the digital equivalent of sticky notes. SSH keys copied to dozens of servers, nobody sure which ones are still active. Database passwords shared in a Slack thread six months ago. Kubernetes kubeconfigs with long-lived tokens sitting in ~/.kube/config forever. A VPN that grants access to everything once you’re inside.
Teleport replaces all of it with short-lived certificates, identity-aware proxying, and a complete audit trail. Every connection goes through the Teleport proxy. Every session is logged. Every credential expires in hours, not years. Access is granted based on who you are and what role you hold — not whether your static key happens to be on the server.
This guide builds a complete Teleport deployment: SSH access to Linux nodes, Kubernetes access, database access, web app proxying, CI/CD with Machine ID, and RBAC with just-in-time access requests.
The Problem with Traditional Infrastructure Access
SSH keys don’t expire, don’t carry identity, and proliferate. An engineer adds their public key to 30 servers on their first week. When they leave, someone has to remember to remove it from all 30. The answer is usually “we’ll get to it.” You have no idea who accessed which server, when, or what they ran.
Database credentials are shared. The DB_PASSWORD in your .env file has been unchanged for three years. Your junior engineer, your senior engineer, and your CI pipeline all use the same credentials. If it leaks, you can’t tell who did what.
Kubernetes kubeconfigs contain long-lived service account tokens. They work forever until explicitly revoked. They’re copied between machines, backed up to Dropbox, emailed to people who need “just temporary” access.
VPNs solve the wrong problem. Once inside the VPN, you have access to everything on the internal network. There’s no per-service authorization, no MFA per resource, no session recording.
Teleport’s model: every credential is a short-lived certificate, issued only after authentication and MFA, carrying your identity and roles, valid for hours, and automatically expiring. The proxy records everything.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Teleport Control Plane │
│ │
│ ┌──────────────────┐ ┌─────────────────────────────┐ │
│ │ Auth Service │ │ Proxy Service │ │
│ │ │ │ │ │
│ │ Certificate CA │◄─────►│ SSH/HTTPS/K8s/DB routing │ │
│ │ RBAC engine │ │ Web UI (:3080) │ │
│ │ Audit storage │ │ Agent tunnel (:3024) │ │
│ │ User identity │ │ Public endpoint │ │
│ └──────────────────┘ └─────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────┘
│ reverse tunnels (outbound)
┌────────────────────┼────────────────────┐
│ │ │
┌──────▼──────┐ ┌────────▼──────┐ ┌───────▼───────┐
│ SSH Agent │ │ K8s Agent │ │ DB Agent │
│ (per node) │ │ (per cluster)│ │ (per DB) │
│ port 3022 │ │ │ │ │
└─────────────┘ └───────────────┘ └───────────────┘
User workflow:
tsh login → SSO/MFA → short-lived certificate (12h)
tsh ssh / tsh kube login / tsh db connect → routed via proxy
Auth Service — The certificate authority. Issues short-lived X.509 certs encoding the user’s identity and roles. Stores all audit events. Validates SSO tokens and MFA. Never exposed directly to users.
Proxy Service — The public entry point. Routes SSH, HTTPS, Kubernetes API, and database connections. Hosts the Web UI. Agents connect to it via outbound reverse tunnels — no inbound firewall rules needed on your servers.
Agents — Teleport processes running alongside your protected resources (Linux nodes, K8s clusters, databases, apps). They establish outbound tunnels to the Proxy and enforce access policy locally.
tsh — The CLI clients use. Handles login, certificate storage in ~/.tsh/, and all resource access commands.
How Certificate Auth Works
The fundamental difference from SSH keys: certificates are short-lived and carry identity.
1. tsh login --proxy=proxy.example.com --user=alice
↓
2. Proxy redirects to SSO provider (Okta, GitHub, Google, etc.)
↓
3. Alice authenticates + completes MFA
↓
4. Auth Service receives confirmation, evaluates Alice's roles
↓
5. Auth Service issues X.509 certificate:
- Subject: alice
- Roles: developer, db-reader
- Logins: ubuntu, deploy
- Valid for: 12 hours
↓
6. Certificate stored in ~/.tsh/keys/proxy.example.com/
↓
7. tsh ssh ubuntu@web-01
→ Presents certificate to Proxy
→ Proxy validates cert (signed by its CA, not expired)
→ Proxy checks RBAC: does 'developer' role allow access to web-01?
→ Forwards to SSH Agent on web-01
→ Agent verifies cert, starts session
→ Everything logged to audit trail
When the certificate expires, access stops. No revocation lists. No “remove the key from the server.” It just stops working and Alice has to re-login. If you terminate someone at 9am, their access is gone before lunch.
Self-Hosted Deployment (Docker Compose)
Directory setup
1
2
|
mkdir -p /opt/teleport/data
cd /opt/teleport
|
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
services:
teleport:
image: public.ecr.aws/gravitational/teleport-ent:16
hostname: teleport
ports:
- "3023:3023" # SSH client connections
- "3024:3024" # SSH reverse tunnel (agents connect here)
- "3025:3025" # Auth Service (internal)
- "3026:3026" # Kubernetes client port
- "3080:3080" # Web UI + HTTPS API
volumes:
- ./teleport.yaml:/etc/teleport.yaml:ro
- ./data:/var/lib/teleport
restart: unless-stopped
healthcheck:
test: ["CMD", "tctl", "status"]
interval: 30s
retries: 3
|
teleport.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
version: v3
teleport:
nodename: teleport
data_dir: /var/lib/teleport
log:
output: stderr
severity: INFO
auth_service:
enabled: yes
cluster_name: homelab
listen_addr: 0.0.0.0:3025
# Authentication method
authentication:
type: local # Use local users + optional SSO
second_factor: otp # Require TOTP MFA
# For GitHub SSO:
# type: github
# Session recording
session_recording: node # Record at the node (default)
proxy_service:
enabled: yes
listen_addr: 0.0.0.0:3023
tunnel_listen_addr: 0.0.0.0:3024
web_listen_addr: 0.0.0.0:3080
# Public address users connect to
public_addr: teleport.example.com:3080
ssh_public_addr: teleport.example.com:3023
tunnel_public_addr: teleport.example.com:3024
kube_public_addr: teleport.example.com:3026
ssh_service:
enabled: yes
labels:
role: teleport-server
env: homelab
# Optionally enable on this same host:
# kubernetes_service:
# enabled: yes
# kube_cluster_name: homelab
|
Start and create initial admin user
1
2
3
4
5
6
7
8
9
10
11
12
13
|
docker compose up -d
# Wait for startup
docker compose logs -f teleport
# Create admin user (generates signup URL)
docker compose exec teleport tctl users add admin \
--roles=editor,access \
--logins=ubuntu,root
# Visit the URL printed — sets up password + TOTP
# Then login:
tsh login --proxy=teleport.example.com --user=admin
|
Enrolling Linux Nodes
Agents run on every server you want to protect. They open outbound tunnels to the proxy — no inbound firewall rules, no SSH port exposed to the internet.
Generate a join token
1
2
3
4
5
6
|
# On the Teleport server
docker compose exec teleport tctl tokens add \
--type=node \
--ttl=30m \
--format=text
# Prints: abc123def456...
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
# Install on the target server
curl https://cdn.teleport.dev/install-linux.sh | sudo bash
# Create /etc/teleport.yaml
sudo tee /etc/teleport.yaml <<'EOF'
version: v3
teleport:
nodename: web-01
data_dir: /var/lib/teleport
auth_token: "abc123def456..." # Token from above
proxy_server: teleport.example.com:3080
auth_service:
enabled: no
proxy_service:
enabled: no
ssh_service:
enabled: yes
listen_addr: 127.0.0.1:3022 # Local only — proxy handles routing
labels:
env: production
team: platform
role: web-server
EOF
sudo systemctl enable --now teleport
sudo systemctl status teleport
|
Verify enrollment
1
2
3
4
5
6
7
8
9
10
11
|
# Back on any machine with tsh logged in
tsh ls
# NAME ADDRESS LABELS
# web-01 127.0.0.1:3022 env=production,team=platform,role=web-server
# Connect
tsh ssh ubuntu@web-01
# Filter by label
tsh ls env=production
tsh ssh ubuntu@env=production # Connects to first matching node
|
Kubernetes Access
The Kubernetes Service acts as a proxy between kubectl and your K8s API servers. Users get short-lived kubeconfig credentials that expire automatically.
Deploy the Teleport agent into the cluster:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
# teleport-kube-agent.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: teleport-kube-agent
namespace: teleport
spec:
replicas: 1
selector:
matchLabels:
app: teleport-kube-agent
template:
metadata:
labels:
app: teleport-kube-agent
spec:
serviceAccountName: teleport-kube-agent
containers:
- name: teleport
image: public.ecr.aws/gravitational/teleport:16
args: ["start", "-c", "/etc/teleport/teleport.yaml"]
volumeMounts:
- name: config
mountPath: /etc/teleport
- name: data
mountPath: /var/lib/teleport
volumes:
- name: config
configMap:
name: teleport-kube-agent-config
- name: data
emptyDir: {}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: teleport-kube-agent-config
namespace: teleport
data:
teleport.yaml: |
version: v3
teleport:
auth_token: "JOIN_TOKEN_HERE"
proxy_server: teleport.example.com:443
auth_service:
enabled: no
proxy_service:
enabled: no
ssh_service:
enabled: no
kubernetes_service:
enabled: yes
kube_cluster_name: prod-cluster
labels:
env: production
region: us-east-1
|
Or use Helm:
1
2
3
4
5
6
7
8
9
|
helm repo add teleport https://charts.releases.teleport.dev
helm install teleport-kube-agent teleport/teleport-kube-agent \
--namespace teleport \
--create-namespace \
--set roles=kube \
--set proxyAddr=teleport.example.com:443 \
--set authToken=JOIN_TOKEN_HERE \
--set kubeClusterName=prod-cluster
|
User workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# List available clusters
tsh kube ls
# Name Labels
# ---- ------
# prod-cluster env=production,region=us-east-1
# staging env=staging
# Login — updates ~/.kube/config with a short-lived cert
tsh kube login prod-cluster
# kubectl now works transparently through the Teleport proxy
kubectl get pods -n production
kubectl logs deployment/api-server -f
kubectl exec -it pod/api-server-xxx -- bash
# Switch clusters
tsh kube login staging
kubectl get nodes
# View what you have access to
tsh status
|
Database Access
The Database Service proxies connections to your databases. Users never get direct network access to the database host — everything goes through Teleport.
Database Service configuration
Add to your teleport.yaml (or a dedicated agent):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
database_service:
enabled: yes
databases:
- name: prod-postgres
description: "Production PostgreSQL"
protocol: postgres
uri: postgres.prod.internal:5432
tls:
mode: verify-full
static_labels:
env: production
team: platform
- name: staging-mysql
protocol: mysql
uri: mysql.staging.internal:3306
tls:
mode: verify-ca
static_labels:
env: staging
- name: prod-redis
protocol: redis
uri: redis.prod.internal:6379
static_labels:
env: production
proxy_service:
enabled: yes
# Database protocol ports
postgres_public_addr: teleport.example.com:5432
mysql_public_addr: teleport.example.com:3306
mongo_public_addr: teleport.example.com:27017
|
Teleport supports: PostgreSQL, MySQL/MariaDB, MongoDB, Redis, ElasticSearch, CockroachDB, Cassandra, Snowflake, DynamoDB, and more.
User workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# List databases you have access to
tsh db ls
# Name Description Labels
# ---- ----------- ------
# prod-postgres Production PostgreSQL env=production,team=platform
# staging-mysql Staging MySQL env=staging
# Connect — tsh launches psql/mysql/mongosh automatically
tsh db connect prod-postgres --db-user=alice --db-name=myapp
# psql opens; Teleport handled auth entirely
# Local tunnel (for GUI clients like DBeaver, TablePlus)
tsh db connect prod-postgres --db-user=alice --tunnel
# Listening on 127.0.0.1:PORT...
# Now connect DBeaver to localhost:PORT, no password needed
# Check active DB sessions
tsh db ls --active
|
No database password is ever issued to the user. Teleport authenticates on their behalf using certificates it manages between the proxy and the database.
Application Access
Protect internal web apps without changing a line of their code. Teleport acts as an authenticating reverse proxy, injecting JWT tokens so apps can know who the user is.
App Service configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
app_service:
enabled: yes
apps:
- name: grafana
description: "Production Grafana"
uri: http://grafana.monitoring.svc.cluster.local:3000
public_addr: grafana.teleport.example.com
labels:
env: production
- name: argocd
uri: https://argocd-server.argocd.svc.cluster.local:443
public_addr: argocd.teleport.example.com
insecure_skip_verify: true # If ArgoCD uses self-signed cert
labels:
env: production
- name: internal-api
uri: http://api.internal:8080
public_addr: api.teleport.example.com
# Rewrite headers: inject user identity
rewrite:
headers:
- name: X-Teleport-User
value: "{{internal.logins}}"
|
User workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# List available apps
tsh app ls
# Name Public Address Labels
# ---- -------------- ------
# grafana grafana.teleport.example.com env=production
# argocd argocd.teleport.example.com env=production
# Open app in browser (sets a session cookie with Teleport JWT)
tsh app login grafana
# Apps receive a JWT in the Teleport-Jwt-Assertion header:
# {
# "sub": "alice",
# "roles": ["developer"],
# "username": "alice",
# "traits": { "logins": ["ubuntu"] }
# }
|
Apps can read this JWT to implement their own authorization — e.g., map roles: ["admin"] to Grafana’s admin role.
RBAC: Roles and Access Control
Every access decision in Teleport flows through roles. Roles define what resources you can reach, as which user, with which constraints.
Role YAML structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
kind: role
version: v7
metadata:
name: developer
description: "Standard developer — dev/staging access only"
spec:
options:
max_session_ttl: 8h
record_session:
default: best_effort
# Uncomment to require MFA for every new session:
# require_session_mfa: true
allow:
# OS users this role can log in as
logins:
- ubuntu
- "{{internal.logins}}" # Expands to logins set on the user account
# Which nodes (matched by label)
node_labels:
env: ["dev", "staging"]
team: "{{external.team}}" # Pulled from SSO claim
# Kubernetes clusters and RBAC groups
kubernetes_labels:
env: ["dev", "staging"]
kubernetes_groups:
- developers
kubernetes_resources:
- kind: pod
namespace: "*"
name: "*"
verbs: ["get", "list", "exec"]
# Databases
db_labels:
env: ["dev", "staging"]
db_names: ["*"]
db_users:
- "{{internal.logins}}"
# Apps
app_labels:
env: ["dev", "staging"]
# Can request elevated roles
request:
roles: [oncall, admin]
max_duration: 4h
reason_required: true
deny:
node_labels:
env: production # Explicit deny overrides any allow
db_labels:
env: production
|
Production-only admin role
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
kind: role
version: v7
metadata:
name: admin
spec:
options:
max_session_ttl: 4h
require_session_mfa: true # Hardware key required per session
record_session:
default: strict # Recording mandatory; deny if unavailable
allow:
logins: [root, ubuntu, admin]
node_labels:
"*": "*" # All nodes
kubernetes_labels:
"*": "*"
kubernetes_groups: [system:masters]
db_labels:
"*": "*"
db_names: ["*"]
db_users: ["*"]
|
Apply roles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Create/update role
tctl create -f developer-role.yaml
# Assign to a user
tctl users update alice --set-roles=developer
# Assign multiple roles
tctl users update alice --set-roles=developer,db-reader
# View effective permissions
tctl users get alice
# List all roles
tctl get roles
|
Access Requests: Just-in-Time Elevation
Rather than giving people standing access to production, Access Requests let developers request elevated roles temporarily with an approval workflow.
Configuration
On the base role (developer), allow requesting admin:
1
2
3
4
5
6
|
spec:
allow:
request:
roles: [admin, oncall]
max_duration: 2h
reason_required: true
|
On the admin role, mark it as requestable:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
kind: role
version: v7
metadata:
name: admin
spec:
allow:
node_labels:
"*": "*"
# Who can review requests for this role
review_requests:
roles: [admin-reviewer]
where: >
contains(request.system_annotations["teams"], reviewer.traits["team"])
|
Workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# Developer needs production access for an incident
tsh request create \
--roles=admin \
--reason="P1 incident: API server OOM, need to check pod logs in prod"
# Request ID: a1b2c3d4-...
# Status: PENDING
# Team lead sees it in Slack (if integrated) or via CLI
tctl request ls
tctl request approve a1b2c3d4 \
--reason="Approved for incident response"
# Developer re-logins to pick up the new certificate
tsh login
# Certificate now includes: Roles: developer, admin (expires in 2h)
# After the incident, access expires automatically
# No manual cleanup required
|
Teleport integrates with Slack, PagerDuty, Jira, and Mattermost for approval notifications in Enterprise/Cloud editions.
Audit Log and Session Recording
Every action flows through the proxy. Teleport records all of it.
What gets logged
user.login — successful authentication
user.logout — session ended
auth.attempt — failed login
certificate_issued — new cert issued
session.start — SSH/DB/K8s session opened
session.end — session closed
exec — command run in a session
scp — file transferred
db.session.start — database session opened
db.session.end — database session closed
db.session.query — SQL query executed
kube.request — kubectl API call
app.session.start — app session started
access_request.create — access request submitted
access_request.review — request approved/denied
Querying audit events
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Recent events (Web UI: https://teleport.example.com/web/audit)
tctl audit events
# Filter by user
tctl audit events --user=alice
# Filter by type
tctl audit events --event=session.start
# Events in a time range
tctl audit events \
--from=2026-04-01 \
--to=2026-04-02 \
--event=exec
|
Session recordings
Full terminal I/O is recorded for SSH sessions. You can replay them exactly as they happened.
1
2
3
4
5
6
7
8
9
10
11
12
|
# List recorded sessions
tctl recordings ls
# Replay in terminal
tsh play <session-id>
# Arrow keys scrub through; space to pause
# Export as JSON (for log aggregation)
tsh play --format=json <session-id>
# Download recording
tctl recordings export <session-id> --output=/tmp/session.tar.gz
|
Export to S3
1
2
3
4
5
6
7
8
9
10
11
|
# teleport.yaml
teleport:
storage:
type: s3
region: us-east-1
bucket: my-teleport-audit
auth_service:
audit_events_uri:
- "s3://my-teleport-audit/events?region=us-east-1"
audit_sessions_uri: "s3://my-teleport-sessions?region=us-east-1"
|
Machine ID: CI/CD Without Stored Credentials
Machine ID replaces the SSH keys and API tokens you store in CI/CD secrets with short-lived certificates generated at job run time. The bot’s identity is proven via the CI platform’s OIDC token — no stored secrets at all.
GitHub Actions (no stored credentials)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Fetch Teleport binaries
uses: teleport-actions/setup@v1
with:
version: "16"
- name: Authenticate to Teleport
uses: teleport-actions/auth@v2
with:
proxy: teleport.example.com:443
token: github-actions-bot # Token name, not value
certificate-ttl: 30m
# Credentials are now available — no secrets stored in GitHub
- name: Deploy to Kubernetes
run: |
tsh kube login prod-cluster
kubectl apply -f k8s/
kubectl rollout status deployment/api-server
- name: Run DB migration
run: |
tsh db connect prod-postgres \
--db-user=deploy \
--db-name=myapp < migrations/latest.sql
- name: Restart app servers
run: |
tsh ssh deploy@app-server-01 "sudo systemctl restart myapp"
|
Bot token configuration on the Teleport side
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# Create the role for the CI bot
tctl create -f - <<'EOF'
kind: role
version: v7
metadata:
name: ci-deploy
spec:
allow:
logins: [deploy]
node_labels:
env: production
role: app-server
kubernetes_labels:
env: production
db_labels:
env: production
db_names: [myapp]
db_users: [deploy]
EOF
# Create the bot (Machine ID identity)
tctl bots add github-actions-bot \
--roles=ci-deploy
# Create the GitHub join token — identity proved by GitHub OIDC
tctl tokens add \
--type=bot \
--bot-name=github-actions-bot \
--github-actions-allow='repo:myorg/myrepo:ref:refs/heads/main' \
--format=text
|
The --github-actions-allow constraint means only the main branch of myorg/myrepo can use this token. A compromised fork or a PR branch cannot request credentials.
MFA Configuration
TOTP setup
1
2
3
4
5
6
7
8
9
10
11
|
# Add TOTP device after login
tsh mfa add
# Choose: TOTP
# Scan QR code with Authenticator app
# Enter code to confirm
# List devices
tsh mfa ls
# Remove old device
tsh mfa rm old-phone
|
WebAuthn / hardware keys (YubiKey, etc.)
1
2
3
4
5
6
7
8
9
|
# Add hardware key
tsh mfa add
# Choose: HARDWARE KEY (WebAuthn)
# Touch key when prompted
# Name it: yubikey-5
# Subsequent logins require a key touch
tsh login --proxy=teleport.example.com
# Enter password → Touch your hardware key
|
Require MFA per session (for sensitive roles)
Add to role options:
1
2
3
|
spec:
options:
require_session_mfa: true # Touch key before every SSH/DB/K8s session
|
With this enabled, even if Alice’s certificate is valid, connecting to any node under this role requires another hardware key touch. Certificates compromised outside the workstation cannot open sessions.
Self-Hosted vs Teleport Cloud
|
Community (self-hosted) |
Enterprise (self-hosted) |
Teleport Cloud |
| Cost |
Free (<100 employees) |
$$ per user/month |
$$$ per user/month |
| HA/clustering |
No |
Yes |
Yes (managed) |
| FIPS 140-2 |
No |
Yes |
Yes |
| Control plane ops |
You |
You |
Gravitational |
| Slack/PagerDuty approval |
No |
Yes |
Yes |
| Access Request UI |
Basic |
Full |
Full |
| Session recording |
Local disk |
S3/GCS |
Managed S3 |
| Global proxy network |
No |
No |
Yes |
| Support |
Community |
SLA |
SLA |
For homelabs and small teams: Community Edition self-hosted covers everything in this guide. For production at scale with compliance requirements, Cloud or Enterprise removes operational overhead on the control plane.
Practical: Quick Homelab Setup
The fastest path to useful security improvement:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 1. Start Teleport (single Docker Compose node)
mkdir /opt/teleport && cd /opt/teleport
# Create teleport.yaml (from above)
docker compose up -d
# 2. Create your admin user
docker compose exec teleport tctl users add yourname \
--roles=editor,access \
--logins=ubuntu,root
# 3. Enroll your most important server
tctl tokens add --type=node --ttl=30m --format=text
# → Install agent on that server with the token
# 4. Login and verify
tsh login --proxy=localhost:3080 --user=yourname
tsh ls
tsh ssh ubuntu@your-server
# 5. Watch the audit log
tctl audit events
|
From here, add nodes progressively, create per-person roles instead of sharing ubuntu broadly, and enable the database service as you go. You don’t have to do it all at once — even “every SSH login is logged with the user’s real identity” is a massive security improvement over shared keys.
Summary
Teleport removes the three worst things about infrastructure access:
- Shared, static credentials → short-lived per-user certificates that expire automatically
- No audit trail → every command, query, and kubectl call logged and recorded
- Binary network access → fine-grained RBAC per resource, per protocol, per label
The same tool handles SSH, Kubernetes, databases, and web apps through a single proxy — so you get one place to manage access, one audit log to query, and one expiry model for all credentials. When someone leaves, you disable their account in Teleport and they lose access to everything immediately, with no SSH key hunting required.
For a homelab, Community Edition is free and handles everything covered here. Start with SSH access to your most critical servers, add the audit log to your monitoring stack, and expand from there.
Comments