Last Updated: March 10, 2026
This document covers admission controllers, pod security standards, secrets encryption at rest, and audit logging.
For authentication and authorization (JWT tokens, RBAC, mTLS client certificates), see AUTHENTICATION.md.
- Admission Controllers
- Pod Security Standards
- Secrets Encryption at Rest
- Audit Logging
- Configuration
- Examples
- Best Practices
Admission controllers intercept API requests before they are persisted to etcd. They can validate requests (reject invalid requests) or mutate requests (modify requests before storage).
API Request
↓
Authentication
↓
Authorization (RBAC)
↓
Admission Controllers ← You are here
↓
Validation
↓
Storage (etcd)
Rusternetes includes several built-in admission controllers that are automatically enabled:
Purpose: Prevents creating resources in non-existent or terminating namespaces.
Behavior:
- Rejects requests to create resources in namespaces that don't exist
- Allows creating resources in system namespaces (kube-system, kube-public, default)
Purpose: Enforces resource consumption limits per namespace.
Status: Framework implemented, needs controller implementation.
Future capabilities:
- Enforce CPU/memory limits per namespace
- Enforce object count limits (pods, services, etc.)
- Prevent namespace from exceeding quota
Purpose: Enforces min/max resource limits and provides defaults.
Status: Framework implemented, needs controller implementation.
Future capabilities:
- Set default requests/limits for containers
- Enforce min/max resource constraints
- Validate resource requests are within limits
Purpose: Enforces Pod Security Standards at namespace level.
See: Pod Security Standards section below.
You can implement custom admission controllers by implementing the AdmissionController trait:
use rusternetes_common::admission::*;
use async_trait::async_trait;
struct MyAdmissionController;
#[async_trait]
impl AdmissionController for MyAdmissionController {
fn name(&self) -> &str {
"MyAdmissionController"
}
async fn admit(&self, request: &AdmissionRequest) -> AdmissionResponse {
// Validate the request
if should_deny(request) {
return AdmissionResponse::Deny("reason".to_string());
}
// Optionally mutate the request
let patches = vec![
PatchOperation {
op: PatchOp::Add,
path: "/metadata/labels/admitted".to_string(),
value: Some(serde_json::json!("true")),
from: None,
}
];
AdmissionResponse::AllowWithPatch(patches)
}
fn supports_operation(&self, operation: &Operation) -> bool {
matches!(operation, Operation::Create | Operation::Update)
}
}Multiple admission controllers run in sequence via an AdmissionChain:
use rusternetes_common::admission::*;
let chain = AdmissionChain::new()
.with_built_in_controllers() // Adds all built-in controllers
.with_controller(Arc::new(MyAdmissionController));
// Run all admission controllers
let response = chain.admit(&request).await;
if !response.is_allowed() {
// Request denied
println!("Denied: {}", response.deny_reason().unwrap());
}Pod Security Standards define three levels of security restrictions for pods:
Use case: Trusted workloads, system components
Restrictions: None (allows everything)
When to use:
- System pods (kube-system namespace)
- Monitoring agents
- Network plugins
- Storage drivers
Use case: General workloads that need some privileges
Restrictions:
- ❌ hostNetwork, hostPID, hostIPC
- ❌ Privileged containers
- ❌ Dangerous Linux capabilities (only allows safe baseline capabilities)
Allowed capabilities (baseline):
- AUDIT_WRITE
- CHOWN
- DAC_OVERRIDE
- FOWNER
- FSETID
- KILL
- MKNOD
- NET_BIND_SERVICE
- SETFCAP
- SETGID
- SETPCAP
- SETUID
- SYS_CHROOT
When to use:
- Web applications
- Databases
- Most application workloads
Use case: Security-critical workloads
Restrictions: All baseline restrictions, plus:
- ❌ Must set runAsNonRoot=true for all containers
- ❌ Must set allowPrivilegeEscalation=false
- ❌ Must drop ALL capabilities
- ❌ Must define seccomp profile
- ❌ Cannot use hostPath volumes
When to use:
- Financial applications
- PCI-DSS compliance workloads
- Multi-tenant environments
- Public-facing APIs
Pod Security Standards are enforced at the namespace level using labels:
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restrictedapiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.25-alpine
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefaultWhen a pod violates a security standard, you get detailed error messages:
Pod violates restricted security standard:
- Container 'app' must set runAsNonRoot=true
- Container 'app' must set allowPrivilegeEscalation=false
- Container 'app' must drop ALL capabilities
- Container 'app' must define seccomp profile
Secrets (and other resources) can be encrypted at rest in etcd using multiple encryption providers.
Algorithm: AES-256-GCM (Galois/Counter Mode)
Features:
- 256-bit encryption
- Authenticated encryption (AEAD)
- Unique nonce per encryption
- Production-ready
Key generation:
# Generate a 256-bit key
openssl rand -base64 32Example configuration:
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-key>Purpose: No encryption (passthrough)
Use case: Testing, migration
Example:
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- identity: {}Status: Framework implemented, AWS KMS stub available
Future capabilities:
- Integration with AWS KMS
- Automatic key rotation
- Envelope encryption
- Audit trail of key usage
Create an encryption configuration file:
# encryption-config.yaml
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
# Try key1 first
- aescbc:
keys:
- name: key1
secret: K7XSxhxZZ7Y5QZ0ckZjW8qY4b2X+J5GvP9N2bXxYqT8=
# Then try key2 (for rotation)
- aescbc:
keys:
- name: key2
secret: dGVzdGtleWRhdGF0ZXN0a2V5ZGF0YXRlc3RrZXlkYQ==
# Finally, try identity (for migration from unencrypted)
- identity: {}
- resources:
- configmaps
providers:
- identity: {} # Don't encrypt ConfigMapsTo rotate encryption keys:
- Add new key to providers list (at the top)
- Restart API server
- Read and write all secrets to re-encrypt with new key:
kubectl get secrets --all-namespaces -o json | \ kubectl replace -f - - Remove old key from configuration
use rusternetes_common::encryption::*;
// Load encryption config
let config_yaml = std::fs::read_to_string("encryption-config.yaml")?;
let config: EncryptionConfig = serde_yaml::from_str(&config_yaml)?;
// Create transformer
let transformer = EncryptionTransformer::from_config(config)?;
// Encrypt a secret
let plaintext = b"my-secret-data";
let ciphertext = transformer.encrypt_for_resource("secrets", plaintext)?;
// Decrypt a secret
let decrypted = transformer.decrypt_for_resource("secrets", &ciphertext)?;
assert_eq!(plaintext, decrypted.as_slice());Audit logging tracks all API requests for security, compliance, and debugging.
No audit logging.
Log request metadata only (no request/response bodies).
Includes:
- Request URI
- HTTP verb
- User information
- Resource being accessed
- Response status code
Example use case: Compliance logging without PII
Log metadata + request body (no response body).
Includes: Everything in Metadata, plus:
- Request body (JSON)
Example use case: Track what users are trying to create/update
Log everything (metadata + request body + response body).
Includes: Everything in Request, plus:
- Response body (JSON)
Example use case: Full audit trail for forensics
Each request goes through multiple stages:
- RequestReceived: Request received by API server
- ResponseStarted: Response headers sent
- ResponseComplete: Response fully sent
- Panic: Handler panicked during request processing
Audit events follow Kubernetes audit.k8s.io/v1 format:
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"level": "Metadata",
"auditID": "550e8400-e29b-41d4-a716-446655440000",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/default/pods",
"verb": "create",
"user": {
"username": "alice",
"uid": "alice-uid",
"groups": ["system:authenticated"]
},
"objectRef": {
"resource": "pods",
"namespace": "default",
"name": "my-pod",
"apiVersion": "v1"
},
"responseStatus": {
"code": 201
},
"requestReceivedTimestamp": "2026-03-10T10:00:00Z",
"stageTimestamp": "2026-03-10T10:00:00.123Z"
}Create an audit policy:
use rusternetes_common::audit::*;
let policy = AuditPolicy {
level: AuditLevel::Metadata,
log_requests: true,
log_responses: true,
log_metadata: true,
};Create an audit backend:
// File backend
let backend = Arc::new(
FileAuditBackend::new("/var/log/kubernetes/audit.log".to_string()).await?
);
// Create logger
let logger = AuditLogger::new(backend, policy);Log requests and responses:
// Log request
let audit_id = logger.log_request(
"/api/v1/pods".to_string(),
"GET".to_string(),
user_info,
None,
).await;
// ... process request ...
// Log response
logger.log_response(
audit_id,
"/api/v1/pods".to_string(),
"GET".to_string(),
user_info,
None,
200,
None,
).await;Audit logs are written as JSON Lines (one JSON object per line):
# View audit logs
tail -f /var/log/kubernetes/audit.log
# Parse with jq
cat /var/log/kubernetes/audit.log | jq '.user.username'
# Find all denied requests
cat /var/log/kubernetes/audit.log | jq 'select(.responseStatus.code >= 400)'
# Find all actions by user
cat /var/log/kubernetes/audit.log | jq 'select(.user.username == "alice")'Add security flags to the API server:
./rusternetes-api-server \
--bind-address 0.0.0.0:6443 \
--etcd-servers http://localhost:2379 \
--tls \
--tls-self-signed \
--enable-admission-plugins "NamespaceLifecycle,PodSecurityStandards" \
--encryption-provider-config /etc/kubernetes/encryption-config.yaml \
--audit-log-path /var/log/kubernetes/audit.log \
--audit-policy-file /etc/kubernetes/audit-policy.yaml# Create namespace with restricted security
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
---
# This pod will be ALLOWED
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.25-alpine
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
---
# This pod will be DENIED (privileged)
apiVersion: v1
kind: Pod
metadata:
name: insecure-app
namespace: production
spec:
containers:
- name: app
image: nginx
securityContext:
privileged: true # VIOLATION!# encryption-config.yaml
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: K7XSxhxZZ7Y5QZ0ckZjW8qY4b2X+J5GvP9N2bXxYqT8=
---
# Create a secret (will be encrypted in etcd)
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
data:
password: cGFzc3dvcmQ= # base64("password")// Initialize audit logger
let backend = Arc::new(
FileAuditBackend::new("/var/log/kubernetes/audit.log".to_string()).await?
);
let policy = AuditPolicy {
level: AuditLevel::Metadata, // No PII in logs
log_requests: true,
log_responses: true,
log_metadata: true,
};
let logger = AuditLogger::new(backend, policy);
// All API requests are now logged- Enable built-in controllers: Always enable NamespaceLifecycle and PodSecurityStandards
- Order matters: Controllers run sequentially; put mutation before validation
- Error messages: Provide clear, actionable error messages in denials
- Performance: Keep admission logic fast (< 100ms per controller)
- Start with baseline: Use baseline for most namespaces
- Restricted for sensitive data: Use restricted for namespaces with sensitive workloads
- Privileged sparingly: Only use privileged for system namespaces
- Test before enforcing: Use
warnmode beforeenforcemode - Document exceptions: If you need privileged pods, document why
- Always encrypt secrets: Use AES-GCM for production
- Rotate keys regularly: Rotate encryption keys every 90 days
- Store keys securely: Never commit encryption keys to git
- Use multiple keys: Keep old keys during rotation period
- Monitor access: Audit who accesses encryption keys
- Start with Metadata: Avoid logging sensitive data
- Rotate logs: Use logrotate to prevent disk fill
- Centralize logs: Send audit logs to SIEM (Splunk, Elasticsearch)
- Alert on anomalies: Monitor for unusual patterns
- Retention policy: Keep audit logs for compliance period (90 days, 1 year, etc.)
Before going to production:
- Enable TLS on API server
- Enable RBAC authorization
- Enable admission controllers (at minimum: NamespaceLifecycle, PodSecurityStandards)
- Configure Pod Security Standards (baseline or restricted)
- Enable secrets encryption at rest (AES-GCM)
- Enable audit logging (at least Metadata level)
- Rotate encryption keys regularly
- Review and update RBAC policies
- Monitor audit logs for anomalies
- Test disaster recovery procedures
Problem: Pods are being rejected with unclear errors
Solution:
- Check admission controller logs:
podman logs rusternetes-api-server | grep admission - Verify namespace labels for Pod Security Standards
- Test pod manifest against security level manually
Problem: Custom admission controller not being called
Solution:
- Verify controller is added to AdmissionChain
- Check controller's
supports_operation()method - Ensure controller is not panicking (check logs)
Problem: Cannot read secrets after enabling encryption
Solution:
- Verify encryption config is valid YAML
- Check that key is base64-encoded
- Ensure API server has read access to encryption config file
- Check API server logs for encryption errors
Problem: Key rotation not working
Solution:
- Ensure new key is first in providers list
- Restart API server after config change
- Re-write all secrets to re-encrypt with new key
- Remove old key only after all secrets are re-encrypted
Problem: Audit log file not being created
Solution:
- Check file path is writable by API server
- Verify parent directory exists
- Check disk space
- Review API server logs for permission errors
Problem: Audit logs too large
Solution:
- Reduce audit level (RequestResponse → Request → Metadata)
- Implement log rotation (logrotate)
- Filter out high-volume endpoints
- Send logs to external system instead of files
- ARCHITECTURE.md - System architecture
- DEPLOYMENT.md - Cluster deployment guide
- AUTHENTICATION.md - Authentication, RBAC, and authorization
- TLS_GUIDE.md - TLS configuration