-
Notifications
You must be signed in to change notification settings - Fork 0
Security
This guide covers security best practices for Displace deployments, from local development to production environments.
flowchart TB
subgraph layers["Security Layers"]
secrets["Secrets Management"]
network["Network Security"]
access["Access Control"]
container["Container Security"]
monitoring["Security Monitoring"]
end
secrets --> network
network --> access
access --> container
container --> monitoring
Key Security Areas:
- Secrets and credential management
- Network security and Cloudflare Tunnel
- Kubernetes RBAC
- Container image security
- Monitoring and auditing
Displace generates a .credentials file for each project containing sensitive values.
Security features:
- Automatically added to
.gitignore - Contains database passwords, API keys, security salts
- File permissions set to
0600(owner read/write only)
# Verify .credentials is git-ignored
cat .gitignore | grep credentials
# Check file permissions
ls -la .credentials
# Should show: -rw------- (600)
# Fix permissions if needed
chmod 600 .credentials# Verify secrets aren't tracked
git status --ignored
# If accidentally committed, remove from history
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .credentials' \
--prune-empty --tag-name-filter cat -- --allFor production environments, use Kubernetes Secrets or external secrets managers:
Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: my-app
type: Opaque
stringData:
DB_PASSWORD: "your-secure-password"
DB_ROOT_PASSWORD: "your-root-password"External Secrets (AWS Secrets Manager, HashiCorp Vault):
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: database-credentials
data:
- secretKey: DB_PASSWORD
remoteRef:
key: myapp/database
property: password# Generate new database password
NEW_PASSWORD=$(openssl rand -base64 32)
# Update .credentials
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$NEW_PASSWORD/" .credentials
# Update Kubernetes secret
kubectl create secret generic database-credentials \
--from-literal=DB_PASSWORD="$NEW_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
# Restart application to pick up new secret
kubectl rollout restart deployment/my-app -n my-namespaceDisplace stores API keys in ~/.displace/auth.json with restricted permissions:
# Check API key file permissions
ls -la ~/.displace/auth.json
# Should show: -rw------- (600)- Never share your API key
- Rotate keys periodically via displace.tech dashboard
- Use separate keys for different environments
- Revoke compromised keys immediately
# Check current authentication
displace auth status
# Logout (removes stored key)
displace auth logout
# Login with new key
displace auth loginDisplace integrates Cloudflare Tunnel for secure ingress without exposing infrastructure.
flowchart LR
internet["Internet"] --> cloudflare["Cloudflare Edge"]
cloudflare -->|"Encrypted Tunnel"| tunnel["cloudflared<br/>(in cluster)"]
tunnel --> service["Application<br/>Service"]
Benefits:
- No public IP addresses required
- DDoS protection included
- SSL/TLS termination at Cloudflare
- Zero Trust security model
Setup:
# Cloudflare Tunnel is installed by default
displace cluster create production --provider aws --cloudflared
# Configure tunnel
displace cloudflare configure --tunnel-token YOUR_TOKENSee Cloudflare Tunnel for detailed configuration.
Restrict pod-to-pod communication:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: my-app
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-wordpress-to-database
namespace: my-app
spec:
podSelector:
matchLabels:
app: mariadb
ingress:
- from:
- podSelector:
matchLabels:
app: wordpress
ports:
- protocol: TCP
port: 3306Always use HTTPS in production:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80Create dedicated service accounts for applications:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: my-app
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-app-role
namespace: my-app
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-app-binding
namespace: my-app
subjects:
- kind: ServiceAccount
name: my-app
namespace: my-app
roleRef:
kind: Role
name: my-app-role
apiGroup: rbac.authorization.k8s.io# In Deployment
spec:
template:
spec:
serviceAccountName: my-app
automountServiceAccountToken: false # Unless neededConfigure pod and container security:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALLsecurityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}Use specific tags (not latest):
# Good
image: wordpress:6.9.4-php8.3-fpm-alpine
# Avoid
image: wordpress:latestScan images for vulnerabilities:
# Using Trivy
trivy image my-app:latest
# Using Docker Scout
docker scout cves my-app:latestUse trusted base images:
# Use official images
FROM php:8.3-fpm-alpine
# Or distroless for minimal attack surface
FROM gcr.io/distroless/php:8.3Add to your nginx configuration:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;If not needed, disable XML-RPC to prevent brute force attacks:
// In wp-config.php or mu-plugin
add_filter('xmlrpc_enabled', '__return_false');Add a plugin or mu-plugin:
// mu-plugins/limit-login-attempts.php
function limit_login_attempts($user, $username, $password) {
$ip = $_SERVER['REMOTE_ADDR'];
$failed_attempts = get_transient('failed_login_' . $ip) ?: 0;
if ($failed_attempts >= 5) {
return new WP_Error('too_many_attempts',
'Too many failed login attempts. Try again in 15 minutes.');
}
return $user;
}
add_filter('authenticate', 'limit_login_attempts', 30, 3);
function track_failed_login($username) {
$ip = $_SERVER['REMOTE_ADDR'];
$failed_attempts = get_transient('failed_login_' . $ip) ?: 0;
set_transient('failed_login_' . $ip, $failed_attempts + 1, 15 * MINUTE_IN_SECONDS);
}
add_action('wp_login_failed', 'track_failed_login');# WordPress recommended permissions
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 600 /var/www/html/wp-config.phpUse SSL for database connections:
// In wp-config.php
define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL);Generated passwords should be:
- At least 32 characters
- Include uppercase, lowercase, numbers, symbols
- Unique per environment
# Generate strong password
openssl rand -base64 32Encrypt database backups:
# Backup with encryption
mysqldump -u wordpress -p wordpress | \
gpg --symmetric --cipher-algo AES256 > backup.sql.gpg
# Restore from encrypted backup
gpg --decrypt backup.sql.gpg | mysql -u wordpress -p wordpress# Use IAM roles instead of access keys when possible
# Follow least privilege principle for IAM policies
displace provider aws policy # Shows required permissions# Use service accounts with minimal permissions
# Store credentials securely
chmod 600 ~/.displace/providers/gcp/service-account.json# Use API tokens with limited scopes
# Rotate tokens periodically
chmod 600 ~/.displace/providers/digitalocean/token# In cluster configuration
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: RequestResponse
resources:
- group: ""
resources: ["pods", "deployments"]Use the built-in monitoring stack:
# Port-forward to Grafana
kubectl port-forward -n monitoring svc/grafana 3000:80
# Check security dashboards
# - Failed authentication attempts
# - Unusual network traffic
# - Resource usage spikes# Prometheus alert rule
groups:
- name: security-alerts
rules:
- alert: PodSecurityViolation
expr: count(kube_pod_status_phase{phase="Running"} and on(pod) kube_pod_container_security_context_privileged{privileged="true"}) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Privileged container detected"-
.credentialsfile has 600 permissions -
.credentialsis in.gitignore - No secrets in git history
- Using strong generated passwords
- HTTPS enabled with valid certificates
- Cloudflare Tunnel configured (or other secure ingress)
- Network policies in place
- RBAC configured with least privilege
- Container security contexts set
- Secrets in Kubernetes Secrets (not ConfigMaps)
- Database using SSL connections
- Audit logging enabled
- Monitoring and alerting configured
- Regular secret rotation
- Image vulnerability scanning
- Security update monitoring
- Backup encryption
- Access review
- Immediately rotate all affected credentials
- Revoke API keys and tokens
- Review access logs for unauthorized activity
- Update all deployed applications
- Notify affected parties if required
# Rotate database password
NEW_PASS=$(openssl rand -base64 32)
kubectl create secret generic db-creds \
--from-literal=password="$NEW_PASS" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/my-app
# Revoke and regenerate API key
displace auth logout
# Generate new key in dashboard
displace auth login- Isolate the cluster (disable ingress)
- Capture logs and state for analysis
- Create a new cluster from scratch
- Redeploy applications with new secrets
- Investigate root cause
- Authentication - API key management
- Cloudflare Tunnel - Secure ingress
- Monitoring - Security monitoring
- Configuration - Secrets configuration