A Kubernetes egress proxy that controls outbound traffic from pods to approved destinations based on pod identity.
egress-guard is an HTTP CONNECT proxy written in Go that enforces egress policies for Kubernetes workloads. It authenticates pods via Proxy-Authorization headers and validates both the requested destination and SNI hostname during TLS handshakes to prevent DNS rebinding attacks.
- API Key-based Access Control: Authenticate via Proxy-Authorization header using exact API key matching
- Flexible Destination Matching: Support for exact domains, wildcard suffixes, IP addresses, and CIDR ranges
- SNI Validation: Extracts and validates SNI hostname from TLS ClientHello to prevent DNS rebinding
- Hot Configuration Reload: Automatically reloads policy changes without restart
- Prometheus Metrics: Comprehensive metrics for monitoring and alerting
- Structured JSON Logging: Audit trail of all connection attempts
- Graceful Shutdown: Handles SIGINT/SIGTERM with connection draining
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ │ CONNECT req │ │ TCP tunnel │ │
│ Pod/Client ├────────────────►│ Egress Proxy ├────────────────►│ Destination │
│ │ + Proxy-Auth │ (Port 4750) │ │ │
└─────────────┘ └──────────────────┘ └─────────────┘
│
│ Metrics/Health
▼
┌──────────────────┐
│ Metrics Server │
│ (Port 9090) │
│ /health │
│ /metrics │
└──────────────────┘
| Variable | Default | Description |
|---|---|---|
PROXY_PORT |
4750 |
Proxy listener port |
METRICS_PORT |
9090 |
Metrics/health endpoint port |
CONFIGMAP_PATH |
/etc/config/egress-policy.yaml |
Path to policy configuration |
WATCH_INTERVAL |
5 |
Config file watch interval (seconds) |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARN, ERROR) |
The policy is defined in a YAML file (typically mounted from a ConfigMap):
identities:
# Frontend services
"frontend-services":
api_keys:
- "frontend-pod-1"
- "frontend-pod-2"
allowed_destinations:
- "api.example.com" # Exact domain
- "*.cdn.example.com" # Wildcard suffix
# Backend services
"backend-services":
api_keys:
- "backend-worker-1"
- "backend-worker-2"
allowed_destinations:
- "*.googleapis.com" # Multiple services
- "db.internal.svc.cluster.local"
- "10.0.0.0/24" # CIDR range
# Monitoring services
"monitoring-services":
api_keys:
- "prometheus"
- "alertmanager"
allowed_destinations:
- "prometheus.internal"
- "192.168.1.100" # Specific IPAPI Key Matching:
- Exact match required (no wildcards)
- API keys must be unique across all identities
Destination Matching (priority order):
- Exact domain (case-insensitive)
- Wildcard suffix (
*.example.commatchesapi.example.com) - CIDR range (
10.0.0.0/24) - Exact IP address
go build -o egress-proxy cmd/main.go# With default config path
./egress-proxy
# With custom config
CONFIGMAP_PATH=./egress-policy.yaml LOG_LEVEL=DEBUG ./egress-proxy# Set up proxy authentication (username = API key)
export PROXY_USER="frontend-pod-1"
export PROXY_PASS="any-password"
# Make request through proxy
curl -x http://localhost:4750 \
-U "$PROXY_USER:$PROXY_PASS" \
https://api.example.com
# Check health
curl http://localhost:9090/health
# View metrics
curl http://localhost:9090/metricsExample deployment (simplified):
apiVersion: v1
kind: ConfigMap
metadata:
name: egress-policy
data:
egress-policy.yaml: |
identities:
"frontend-services":
api_keys: ["frontend-pod-1"]
allowed_destinations: ["api.example.com"]
"backend-services":
api_keys: ["backend-worker-1"]
allowed_destinations: ["*.googleapis.com"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: egress-proxy
spec:
selector:
matchLabels:
app: egress-proxy
template:
metadata:
labels:
app: egress-proxy
spec:
containers:
- name: egress-proxy
image: your-registry/egress-proxy:latest
ports:
- containerPort: 4750
name: proxy
- containerPort: 9090
name: metrics
env:
- name: LOG_LEVEL
value: "INFO"
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: true
volumes:
- name: config
configMap:
name: egress-policyPrometheus metrics exposed on /metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
egress_proxy_requests_total |
Counter | identity, destination, status | Total proxy requests |
egress_proxy_config_reloads_total |
Counter | - | Successful config reloads |
egress_proxy_config_load_errors_total |
Counter | - | Config load errors |
egress_proxy_allowed_destinations |
Gauge | identity | Allowed destinations per identity |
Status Values: allowed, denied, auth_required, bad_request, connection_failed, sni_mismatch
The proxy extracts the Server Name Indication (SNI) hostname from the TLS ClientHello and validates it against the allowed destinations. This prevents DNS rebinding attacks where an attacker:
- Requests connection to an allowed domain
- DNS resolves to a malicious IP
- TLS handshake uses a different (non-allowed) hostname
If SNI validation fails, the connection is rejected with 403 Forbidden.
Pods authenticate using the Proxy-Authorization: Basic <base64> header, where the username represents the pod identity. The password is ignored (can be any value).
All connection attempts are logged in structured JSON format with:
- Timestamp
- Identity
- Destination
- Action (allowed/denied)
- Error details (if applicable)
Never logged: Proxy credentials, TLS keys, or sensitive headers
Run the test suite:
# All tests
go test ./...
# Specific package
go test ./internal/config
# With coverage
go test -cover ./...
# Verbose output
go test -v ./...egress-guard/
├── cmd/
│ └── main.go # Application entry point
├── internal/
│ ├── config/ # Configuration management
│ │ ├── config.go
│ │ └── config_test.go
│ ├── logging/ # Structured logging
│ │ └── logger.go
│ ├── metrics/ # Prometheus metrics
│ │ └── metrics.go
│ └── proxy/ # HTTP CONNECT proxy
│ ├── server.go
│ ├── request_handler.go
│ ├── tls_handler.go
│ └── bytes_counting_writer.go
├── e2e/ # End-to-end tests
│ └── server_e2e_test.go
├── egress-policy.yaml # Example configuration
├── CONTRIBUTING.md
├── LICENSE
├── go.mod
└── README.md
- Go 1.21+
- Access to a Kubernetes cluster (for deployment)
- Update the appropriate package in
internal/ - Add tests in
*_test.gofiles - Update metrics if needed
- Update this README
- Test locally before deploying
Enable debug logging:
LOG_LEVEL=DEBUG ./egress-proxyThis will log:
- Detailed request processing
- Config reload events
- SNI validation steps
- Connection lifecycle
Apache 2.0 — see LICENSE.
See CONTRIBUTING.md.