Skip to content

Security

Eric Mann edited this page Mar 19, 2026 · 2 revisions

Security Best Practices

This guide covers security best practices for Displace deployments, from local development to production environments.

Security Overview

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
Loading

Key Security Areas:

  • Secrets and credential management
  • Network security and Cloudflare Tunnel
  • Kubernetes RBAC
  • Container image security
  • Monitoring and auditing

Secrets Management

The .credentials File

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

Never Commit Secrets

# 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 -- --all

Production Secrets

For 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

Rotating Secrets

# 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-namespace

API Key Security

Secure Storage

Displace stores API keys in ~/.displace/auth.json with restricted permissions:

# Check API key file permissions
ls -la ~/.displace/auth.json
# Should show: -rw------- (600)

API Key Best Practices

  1. Never share your API key
  2. Rotate keys periodically via displace.tech dashboard
  3. Use separate keys for different environments
  4. Revoke compromised keys immediately
# Check current authentication
displace auth status

# Logout (removes stored key)
displace auth logout

# Login with new key
displace auth login

Network Security

Cloudflare Tunnel

Displace 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"]
Loading

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_TOKEN

See Cloudflare Tunnel for detailed configuration.

Network Policies

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: 3306

TLS Configuration

Always 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: 80

Kubernetes RBAC

Service Accounts

Create 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

Principle of Least Privilege

# In Deployment
spec:
  template:
    spec:
      serviceAccountName: my-app
      automountServiceAccountToken: false  # Unless needed

Container Security

Security Context

Configure 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:
            - ALL

Read-Only Root Filesystem

securityContext:
  readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
  mountPath: /tmp
- name: cache
  mountPath: /var/cache
volumes:
- name: tmp
  emptyDir: {}
- name: cache
  emptyDir: {}

Image Security

Use specific tags (not latest):

# Good
image: wordpress:6.9.4-php8.3-fpm-alpine

# Avoid
image: wordpress:latest

Scan images for vulnerabilities:

# Using Trivy
trivy image my-app:latest

# Using Docker Scout
docker scout cves my-app:latest

Use trusted base images:

# Use official images
FROM php:8.3-fpm-alpine

# Or distroless for minimal attack surface
FROM gcr.io/distroless/php:8.3

WordPress Security

Security Headers

Add 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;

Disable XML-RPC

If not needed, disable XML-RPC to prevent brute force attacks:

// In wp-config.php or mu-plugin
add_filter('xmlrpc_enabled', '__return_false');

Limit Login Attempts

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');

File Permissions

# 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.php

Database Security

Secure Connections

Use SSL for database connections:

// In wp-config.php
define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL);

Strong Passwords

Generated passwords should be:

  • At least 32 characters
  • Include uppercase, lowercase, numbers, symbols
  • Unique per environment
# Generate strong password
openssl rand -base64 32

Backup Security

Encrypt 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

Cloud Provider Security

AWS

# Use IAM roles instead of access keys when possible
# Follow least privilege principle for IAM policies
displace provider aws policy  # Shows required permissions

GCP

# Use service accounts with minimal permissions
# Store credentials securely
chmod 600 ~/.displace/providers/gcp/service-account.json

DigitalOcean

# Use API tokens with limited scopes
# Rotate tokens periodically
chmod 600 ~/.displace/providers/digitalocean/token

Security Monitoring

Enable Audit Logging

# 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"]

Monitor for Anomalies

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

Alert on Security Events

# 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"

Security Checklist

Development

  • .credentials file has 600 permissions
  • .credentials is in .gitignore
  • No secrets in git history
  • Using strong generated passwords

Staging/Production

  • 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

Ongoing

  • Regular secret rotation
  • Image vulnerability scanning
  • Security update monitoring
  • Backup encryption
  • Access review

Incident Response

If Secrets Are Compromised

  1. Immediately rotate all affected credentials
  2. Revoke API keys and tokens
  3. Review access logs for unauthorized activity
  4. Update all deployed applications
  5. 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

If Cluster Is Compromised

  1. Isolate the cluster (disable ingress)
  2. Capture logs and state for analysis
  3. Create a new cluster from scratch
  4. Redeploy applications with new secrets
  5. Investigate root cause

Related Documentation

Clone this wiki locally