A kubectl plugin that provides passwordless authentication to Kubernetes clusters using SSH keys and Dex Identity Provider with SSH connector support.
Note: This repository contains only the kubectl plugin. The server-side SSH connector for Dex is maintained in a separate fork at github.com/nikogura/dex and is intended for upstream contribution to Dex.
This plugin eliminates the need for passwords, browser-based OAuth flows, or manually managing tokens by leveraging your existing SSH infrastructure. It combines SSH key authentication with OIDC to provide seamless Kubernetes access.
- ✅ Passwordless: Uses SSH keys from ssh-agent or filesystem
- ✅ No Browser Required: Direct CLI authentication
- ✅ Flexible SSH Keys: Works with ssh-agent, filesystem keys, or encrypted keys
- ✅ Standard SSH Behavior: Follows SSH client key discovery and iteration
- ✅ Passphrase Support: Interactive prompts for encrypted private keys
- ✅ Hardware Security: Supports hardware-backed SSH keys (PKCS#11, PIV cards)
- ✅ Centralized Identity: Integrates with Dex for user/group management
- ✅ Standard OIDC: Works with any Kubernetes cluster supporting OIDC
graph LR
A[kubectl] --> B[kubectl-ssh-oidc plugin]
B --> C[ssh-agent]
B --> F[~/.ssh/id_*]
B --> D[Dex IDP]
D --> E[Kubernetes API Server]
C -.->|SSH Key Signature| B
F -.->|SSH Key Signature| B
D -.->|OIDC Token| B
B -.->|ExecCredential| A
Authentication Flow:
- User runs
kubectlcommand - kubectl calls
kubectl-ssh-oidcplugin - Plugin discovers SSH keys from ssh-agent and/or filesystem (standard SSH locations)
- Plugin creates JWT with dual audience model:
aud: Dex instance ID (ensures JWT is for correct Dex instance)target_audience: Desired audience for final OIDC tokens (optional)- Standard claims:
sub,iss,jti,exp,iat,nbf
- Plugin signs JWT directly using SSH private key (follows jwt-ssh-agent pattern)
- Plugin exchanges signed JWT with Dex using OAuth2 Token Exchange (RFC 8693)
- Dex validates JWT signature against administrator-configured SSH keys and returns OIDC token
- kubectl uses OIDC token to authenticate with Kubernetes API
Security Model: JWT contains no SSH keys or fingerprints - it's just a packaging format. All SSH keys and user mappings are configured by administrators in Dex, preventing key injection attacks.
# Download latest release for your platform
# Linux AMD64
curl -L "https://github.com/nikogura/kubectl-ssh-oidc/releases/latest/download/kubectl-ssh_oidc-linux-amd64" -o kubectl-ssh-oidc
# Linux ARM64 (Raspberry Pi, AWS Graviton, etc.)
curl -L "https://github.com/nikogura/kubectl-ssh-oidc/releases/latest/download/kubectl-ssh_oidc-linux-arm64" -o kubectl-ssh-oidc
# macOS AMD64 (Intel)
curl -L "https://github.com/nikogura/kubectl-ssh-oidc/releases/latest/download/kubectl-ssh_oidc-darwin-amd64" -o kubectl-ssh-oidc
# macOS ARM64 (Apple Silicon)
curl -L "https://github.com/nikogura/kubectl-ssh-oidc/releases/latest/download/kubectl-ssh_oidc-darwin-arm64" -o kubectl-ssh-oidc
# Windows AMD64 (PowerShell)
# curl.exe -L "https://github.com/nikogura/kubectl-ssh-oidc/releases/latest/download/kubectl-ssh_oidc-windows-amd64.exe" -o kubectl-ssh-oidc.exe
# Make executable and install (Linux/macOS)
chmod +x kubectl-ssh-oidc
sudo mv kubectl-ssh-oidc /usr/local/bin/# Install directly from GitHub (requires Go 1.21+)
go install github.com/nikogura/kubectl-ssh-oidc@latestNote: The binary will be installed to $GOPATH/bin/kubectl-ssh-oidc or $HOME/go/bin/kubectl-ssh-oidc. Ensure this directory is in your PATH.
git clone https://github.com/nikogura/kubectl-ssh-oidc
cd kubectl-ssh-oidc
# Build and install to user directory
make install
# Or install system-wide (requires sudo)
make install-systemThe plugin supports multiple SSH key sources and follows standard SSH client behavior:
# Start ssh-agent (if not running)
eval $(ssh-agent -s)
# Add your SSH key
ssh-add ~/.ssh/id_ed25519
# Verify keys are loaded
ssh-add -l# Plugin automatically discovers keys from standard locations:
# ~/.ssh/id_ed25519, ~/.ssh/id_rsa, ~/.ssh/id_ecdsa, etc.
# For encrypted keys, you'll be prompted for passphrase:
# Enter passphrase for /home/user/.ssh/id_ed25519: [hidden]
# Custom key paths via environment variable:
export SSH_KEY_PATHS="/path/to/key1:/path/to/key2"
export SSH_USE_AGENT=false # Disable agent, use only filesystem# Plugin tries agent keys first, then filesystem keys
# This is the default behavior - no configuration needed
export SSH_USE_AGENT=true # Default: trueThe SSH connector requires users, and their public keys to be configured.
Alternatively, you can use the complete public key content from your .pub files:
# Copy the entire content of your public key file
cat ~/.ssh/id_ed25519.pub
cat ~/.ssh/id_rsa.pubExample output:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKj8v5Z2b7N4T... user@hostname
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC9Uxzcz0x... user@hostname
Notes about SSH public key format:
- ✅ Algorithm and key data are required:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... - ✅ Comment/hostname is optional:
ssh-ed25519 AAAAC3... user@hostnameor justssh-ed25519 AAAAC3... - ✅ Both formats can be mixed in the same user configuration
- ✅ The connector ignores the 3rd comment field.
Generate secure client credentials for the Dex static client:
# Generate a secure client ID (32 character hex string)
openssl rand -hex 16
# Generate a secure client secret (base64 encoded)
openssl rand -base64 32Important: This kubectl plugin requires a Dex instance with SSH connector support. The SSH connector is not yet available in upstream Dex. You must use the Dex fork at github.com/nikogura/dex that includes the SSH connector implementation.
Create or update your Dex configuration (use the public keys from step 2 and credentials from step 3):
# dex-config.yaml
issuer: https://dex.example.com
staticClients:
- id: your-generated-client-id # Generate secure random client ID
name: 'kubectl SSH OIDC Plugin'
secret: your-generated-client-secret # Generate secure random client secret
connectors:
- type: ssh
id: ssh
name: SSH Key Authentication
config:
# Multiple keys per user - SUPPORTS BOTH FORMATS
users:
"john.doe":
keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample... user@hostname"
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC9Uxzcz0x... user@hostname"
username: "john.doe"
email: "john.doe@example.com"
full_name: "John Doe"
groups:
- "developers"
- "kubernetes-users"
"jane.smith":
keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAnother... jane@hostname"
username: "jane.smith"
email: "jane.smith@example.com"
full_name: "Jane Smith"
groups:
- "developers"
- "team-leads"
allowed_issuers:
- "kubectl-ssh-oidc"
# NEW: Dual audience model for secure instance validation
dex_instance_id: "https://dex.example.com" # Ensures JWTs are for this Dex instance
allowed_target_audiences: # Controls final token audiences
- "your-generated-client-id" # Must match staticClients configuration
- "kubectl" # Standard kubectl client ID
# DEPRECATED: Legacy single-audience support (for backward compatibility)
allowed_clients:
- "your-generated-client-id" # Still supported during migration
default_groups:
- "authenticated"
token_ttl: 3600 # Optional: Token lifetime in seconds (defaults to 3600 if not specified)Important: You must use the Dex fork at github.com/nikogura/dex which includes the SSH connector implementation.
# 1. Clone the Dex fork with SSH connector
git clone https://github.com/nikogura/dex
cd dex
# 2. Build the Dex binary
make build
# 3. The binary will be in bin/dex
./bin/dex version# Build Docker image with SSH connector
docker build -t dex-ssh:latest .
# Tag for your registry (optional)
docker tag dex-ssh:latest your-registry.com/dex-ssh:v2.44.0
# Push to registry (optional)
docker push your-registry.com/dex-ssh:v2.44.0Option A: Docker
# Run with your config file
docker run -d \
-p 5556:5556 \
-v /path/to/your/dex-config.yaml:/etc/dex/config.yaml \
dex-ssh:latest \
serve /etc/dex/config.yamlOption B: Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
spec:
replicas: 1
selector:
matchLabels:
app: dex
template:
metadata:
labels:
app: dex
spec:
containers:
- name: dex
image: your-registry.com/dex-ssh:v2.44.0
ports:
- containerPort: 5556
command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
volumeMounts:
- name: config
mountPath: /etc/dex/cfg
volumes:
- name: config
configMap:
name: dex-configOption C: systemd Service
# Copy binary to system location
sudo cp bin/dex /usr/local/bin/
# Create systemd service
sudo tee /etc/systemd/system/dex.service << EOF
[Unit]
Description=Dex OIDC Identity Provider
After=network.target
[Service]
Type=simple
User=dex
Group=dex
ExecStart=/usr/local/bin/dex serve /etc/dex/config.yaml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Enable and start service
sudo systemctl enable dex
sudo systemctl start dexThe SSH connector supports both legacy direct token authentication and OAuth2 Token Exchange (RFC 8693).
Update your kube-apiserver to accept OIDC tokens:
# kube-apiserver configuration
apiServer:
extraArgs:
oidc-issuer-url: "https://dex.example.com"
oidc-client-id: "kubernetes"
oidc-username-claim: "email"
oidc-groups-claim: "groups"Update your kubeconfig with environment variables for secure credential management:
apiVersion: v1
kind: Config
users:
- name: ssh-oidc-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: kubectl-ssh-oidc
env:
- name: DEX_URL
value: "https://dex.example.com"
- name: CLIENT_ID
value: "your-client-id" # Generated client ID from Dex config
- name: CLIENT_SECRET
value: "your-client-secret" # Generated client secret from Dex config
- name: DEX_INSTANCE_ID
value: "https://dex.example.com" # NEW: Dex instance ID for security
- name: TARGET_AUDIENCE
value: "your-client-id" # NEW: Target audience for tokens
- name: KUBECTL_SSH_USER
value: "your-username"
contexts:
- name: ssh-oidc-context
context:
cluster: your-cluster
user: ssh-oidc-userThe plugin requires a username for the JWT sub claim to identify which user to authenticate in Dex. You can specify this in three ways:
-
Command line argument (3rd argument):
kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc your-username
-
Environment variable:
export KUBECTL_SSH_USER=your-username kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc -
System username fallback: If neither is provided, uses your system username (
$USER)
Important: The username must match a user configured in your Dex SSH connector configuration.
# Use the SSH OIDC context
kubectl config use-context ssh-oidc-context
# Now all kubectl commands authenticate via SSH
kubectl get pods
kubectl get nodes
kubectl logs deployment/my-app# Authentication settings
export DEX_URL="https://dex.example.com"
export CLIENT_ID="your-generated-client-id" # From Dex staticClients configuration
export CLIENT_SECRET="your-client-secret" # From Dex staticClients configuration
export DEX_INSTANCE_ID="https://dex.example.com" # NEW: Dex instance ID for security
export TARGET_AUDIENCE="your-generated-client-id" # NEW: Target audience for final tokens
export AUDIENCE="kubernetes" # DEPRECATED: Legacy audience support
export CACHE_TOKENS="true"
export KUBECTL_SSH_USER="your-username" # Username for authentication
# SSH behavior control
export SSH_USE_AGENT="true" # Use SSH agent (default: true)
export SSH_IDENTITIES_ONLY="false" # Only use specified keys (default: false)
export SSH_KEY_PATHS="/path/to/key1:/path/to/key2" # Custom SSH key paths# Generate credentials manually (uses agent + filesystem keys)
kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc your-username
# Use only filesystem keys (no agent)
SSH_USE_AGENT=false kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc your-username
# Use specific key only
SSH_KEY_PATHS="/home/user/.ssh/id_ed25519" SSH_IDENTITIES_ONLY=true \
kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc your-username
# Using environment variable for username
export KUBECTL_SSH_USER=your-username
kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidcCreate RBAC rules for your users and groups:
# Developer access
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ssh-oidc-developers
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: edit
subjects:
- kind: Group
name: "developers"
apiGroup: rbac.authorization.k8s.io
---
# Admin access
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ssh-oidc-admins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: "kubernetes-admins"
apiGroup: rbac.authorization.k8s.io- Go 1.21+
- SSH agent with loaded keys
- Running Dex instance
# Build for current platform
make build
# Cross-compile for all platforms
make build-all
# Run tests
make test
# Run integration tests (includes unit tests + lint as prerequisites)
make test-integration-local
# Run all tests (unit + lint + full integration with Docker)
make test-all
# Lint code
make lintkubectl-ssh-oidc/
├── main.go # Main plugin executable
├── pkg/
│ ├── kubectl/ # kubectl plugin implementation
│ │ └── mocks/ # Mock objects for testing
├── test/ # Test tools and example configuration
│ ├── dex-config.yaml # Example Dex configuration with SSH connector
│ └── test.go # Standalone test tool for OAuth2 Token Exchange
│ └── helpers.go # Test helper functions and mock data
├── test_integration.go # End-to-end integration tests
├── Makefile # Build automation
├── README.md # This file
├── Usage.md # Usage documentation
└── go.mod # Go module definition
Note: The Dex SSH connector implementation is maintained in the separate fork at github.com/nikogura/dex.
make check-ssh| Issue | Solution |
|---|---|
No SSH keys found |
Ensure keys in ~/.ssh/ or add to agent: ssh-add ~/.ssh/id_ed25519 |
SSH agent not running |
eval $(ssh-agent -s) or use SSH_USE_AGENT=false |
signature type ssh-ed25519 for key type ssh-rsa |
Fixed in v0.0.18+: SSH signature verification now correctly matches key types. Update to latest version. |
JWT token validation failed / Unauthorized |
Fixed in latest: Implemented multiple token authentication system where Dex returns tokens signed with all available keys, allowing kubectl to select the correct token that matches Kubernetes API server expectations. |
Key not authorized in Dex |
Check public key is configured in Dex |
User not found in Dex |
Set username: kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc your-username or export KUBECTL_SSH_USER=your-username |
Passphrase prompt fails |
Ensure TTY available or use unencrypted keys |
OIDC validation failed |
Verify kube-apiserver OIDC settings |
Permission denied |
Check RBAC configuration |
Multiple key errors |
Check detailed error output for each key attempt |
# Enable debug output
export DEBUG=true
kubectl-ssh-oidc https://dex.example.com kubectl-ssh-oidc your-username
# Check what username will be used
echo "Username: ${KUBECTL_SSH_USER:-$(whoami)}"A standalone test client is provided to demonstrate and test the OAuth2 Token Exchange flow:
# Build the test tool
cd test
go build -o test test.go
# Test authentication flow (requires SSH agent with loaded key)
./test https://dex.example.com username
# Example output:
# Testing kubectl-ssh-oidc authentication
# ===================================
# Dex URL: https://dex.example.com
# Username: username
# SSH Agent: $SSH_AUTH_SOCK = /tmp/ssh-agent.sock
#
# 1. Creating SSH-signed JWT...
# ✅ SSH JWT created
#
# 2. Exchanging SSH JWT via OAuth2 Token Exchange...
# ✅ Token exchange successful
#
# 3. Results:
# Access Token: eyJhbGciOiJSUzI1NiIsImtpZCI6...
# ID Token: eyJhbGciOiJSUzI1NiIsImtpZCI6...
#
# 4. Validating ID token structure:
# - Algorithm: RS256 ✅
# - Subject: username ✅
# - Issuer: https://dex.example.com ✅
# - Audience: kubernetes ✅
# - Groups: [admin, developers] ✅
# - Email: username@example.com ✅
# ✅ ID token is valid for Kubernetes
#
# 🎉 kubectl-ssh-oidc authentication test successful!Requirements:
- SSH agent running with authorized key loaded
- Dex instance running with SSH connector configured
- User configured in Dex SSH connector configuration
The kubectl-ssh-oidc plugin now supports a new dual audience model for enhanced security while maintaining backward compatibility with existing deployments.
- Enhanced Security: Prevents JWT replay attacks across different Dex instances
- Flexible Token Audiences: Control what audiences can be requested in final OIDC tokens
- Zero Downtime: Gradual migration path with full backward compatibility
connectors:
- type: ssh
config:
# NEW: Add dual audience fields
dex_instance_id: "https://dex.example.com" # Your Dex issuer URL
allowed_target_audiences: # Migrate from allowed_clients
- "kubectl"
- "your-client-id"
# KEEP: Legacy field for backward compatibility during migration
allowed_clients:
- "your-client-id" # Existing clients continue working# NEW dual-audience kubeconfig (recommended)
users:
- name: ssh-oidc-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: kubectl-ssh-oidc
env:
- name: DEX_INSTANCE_ID
value: "https://dex.example.com" # NEW: Ensures JWT is for this Dex instance
- name: TARGET_AUDIENCE
value: "kubectl" # NEW: Desired audience for final tokens
- name: DEX_URL
value: "https://dex.example.com"
- name: CLIENT_ID
value: "your-client-id"
# ... other existing configMonitor your Dex logs to see migration progress:
SSH_AUDIT: type=token_type username=alice status=info details="processing new_dual_audience token"
SSH_AUDIT: type=token_type username=bob status=info details="processing legacy_single_audience token"
Once all clients show "new_dual_audience" in logs:
# Remove legacy fields from Dex configuration
connectors:
- type: ssh
config:
dex_instance_id: "https://dex.example.com"
allowed_target_audiences:
- "kubectl"
- "your-client-id"
# allowed_clients can now be removedIf issues occur during migration:
- Keep
allowed_target_audiencesconfigured - Legacy clients continue working unchanged
- No service interruption during rollback
This plugin implements a secure JWT-based authentication model designed to prevent common security vulnerabilities:
JWT is Just a Packaging Format: JWTs contain no trusted data until cryptographic verification succeeds against keys configured by Dex administrators.
Administrative Control Model: The Dex configuration provides complete access control:
- WHO can connect: Only users explicitly configured in Dex can authenticate
- HOW they prove identity: Each user's configured SSH keys define which private keys can cryptographically prove the user's identity
- WHAT they can access: User configuration determines scopes (email, groups, permissions)
Identity Claim and Proof: Users prove their identity through a two-step process:
- Identity Claim: User sets the
subfield in the JWT to claim their identity - Cryptographic Proof: User signs the JWT with their SSH private key to prove they control that identity
Security Separation: Authentication (cryptographic proof via SSH key signature) is completely separated from authorization (administrative policy configured in Dex), preventing clients from influencing their own permissions.
No Key Injection: JWTs cannot contain verification keys that clients control - all trusted SSH keys and user mappings are configured by Dex administrators, preventing privilege escalation attacks.
- SSH Key Security: Use strong key types (Ed25519, RSA 4096+, ECDSA P-384)
- Key Rotation: Regularly rotate SSH keys and update Dex configuration
- Hardware Keys: Consider using hardware-backed SSH keys (YubiKey, etc.)
- Network Security: Always use TLS for Dex and Kubernetes API communications
- Audit Logging:
- Enable audit logging in Kubernetes for authentication events
- SSH connector provides comprehensive audit logs with structured format:
SSH_AUDIT: type=auth_success username=john.doe key=ssh-ed25519 issuer=kubectl-ssh-oidc status=success details="user john.doe authenticated with ssh-ed25519 key" - Logs both successful authentications and failed attempts with detailed reasons
- Includes username, SSH key identifier, issuer, and status for security monitoring
- Principle of Least Privilege: Use RBAC to limit user permissions
| Platform | Architecture | Status |
|---|---|---|
| Linux | amd64 | ✅ Supported |
| Linux | arm64 | ✅ Supported |
| macOS | amd64 (Intel) | ✅ Supported |
| macOS | arm64 (Apple Silicon) | ✅ Supported |
| Windows | amd64 | ✅ Supported |
This project includes:
- ✅ kubectl plugin: Complete OAuth2 Token Exchange implementation in
pkg/kubectl/ - ✅ Comprehensive tests: Unit tests and integration tests
- ✅ Cross-platform builds: Automated build pipeline
- ✅ Documentation: Usage examples and configuration guides
⚠️ Binary releases: Set up GitHub Actions for automated releases
Dex SSH connector: Available in the separate fork at github.com/nikogura/dex
- kubectl: v1.20+
- Go: 1.21+ (for building from source)
- SSH Keys: SSH agent or filesystem keys (OpenSSH format)
- Dex: v2.39.1+ with custom SSH connector (see Usage.md for setup)
- Kubernetes: v1.20+ with OIDC support configured
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Dex Identity Service for the extensible OIDC provider
- jwt-ssh-agent-go for SSH agent JWT concepts
- The Kubernetes community for the exec credential plugin interface
- 📖 Usage Documentation
- 🏠 Architecture & Technical Details
⚠️ Limitations & Known Issues- 🐛 Issue Tracker
- 💬 Discussions
Made with ❤️ for the Kubernetes community