Deploying in Kubernetes

ARMORRED images are designed for secure Kubernetes deployments. They run as non-root, support read-only root filesystems, and comply with the Kubernetes Pod Security Standards restricted profile. This guide provides production-ready manifests for deploying ARMORRED images with appropriate security contexts, health checks, and resource management.

Deployment: NGINX Hardened

HTTP probes are the recommended health check pattern because they work regardless of whether a shell is present in the image.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
    armorred.org/tier: hardened
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
        armorred.org/tier: hardened
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        runAsGroup: 65534
        fsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: nginx
          image: ghcr.io/armorred/nginx:1.26-hardened
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 500m
              memory: 128Mi
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: logs
              mountPath: /var/log/nginx
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 64Mi
        - name: logs
          emptyDir:
            sizeLimit: 128Mi

Service

Expose the deployment through a Service targeting port 8080:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    app: nginx
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP
  type: ClusterIP

Clients connect to the Service on port 80, which routes to the container's port 8080.

Health Checks

ARMORRED nginx images expose a /health endpoint that returns HTTP 200:

location /health {
    access_log off;
    return 200 "OK\n";
    add_header Content-Type text/plain;
}

HTTP Probes (Recommended)

HTTP probes work with all ARMORRED images regardless of shell availability:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
readinessProbe:
  httpGet:
    path: /health
    port: 8080

Exec Probes

If the specific image includes a shell, exec probes are also available. However, HTTP probes are preferred for consistency and lower overhead:

readinessProbe:
  exec:
    command:
      - /bin/sh
      - -c
      - "curl -sf http://localhost:8080/health || exit 1"

Check the image analysis pages to verify whether a specific image and tier includes a shell before relying on exec probes.

Writable Directories

ARMORRED images run with readOnlyRootFilesystem: true. Mount emptyDir volumes for directories that require write access:

Mount PathPurposeRecommended Size
/tmpNginx temp files (client body, proxy, fastcgi)64Mi
/var/log/nginxAccess and error logs (if not using /dev/stdout)128Mi

The nginx configuration directs temporary files to /tmp:

client_body_temp_path /tmp/client_body;
proxy_temp_path /tmp/proxy;
fastcgi_temp_path /tmp/fastcgi;
uwsgi_temp_path /tmp/uwsgi;
scgi_temp_path /tmp/scgi;

Custom Configuration

Mount a ConfigMap to override the default nginx configuration:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  nginx.conf: |
    worker_processes auto;
    pid /tmp/nginx.pid;
    error_log /dev/stderr warn;

    events {
        worker_connections 1024;
        use epoll;
    }

    http {
        server_tokens off;
        access_log /dev/stdout;
        sendfile on;
        keepalive_timeout 65;

        client_body_temp_path /tmp/client_body;
        proxy_temp_path /tmp/proxy;
        fastcgi_temp_path /tmp/fastcgi;
        uwsgi_temp_path /tmp/uwsgi;
        scgi_temp_path /tmp/scgi;

        server {
            listen 8080;
            server_name _;

            location / {
                root /var/www/html;
                index index.html;
            }

            location /health {
                access_log off;
                return 200 "OK\n";
                add_header Content-Type text/plain;
            }
        }
    }

Reference the ConfigMap in the Deployment:

volumeMounts:
  - name: config
    mountPath: /etc/nginx/nginx.conf
    subPath: nginx.conf
    readOnly: true

volumes:
  - name: config
    configMap:
      name: nginx-config

Note: The ARMORRED nginx binary expects its configuration at a Nix store path by default. When mounting a custom config, override the command:

command: ["/nix/store/.../bin/nginx"]
args: ["-c", "/etc/nginx/nginx.conf", "-g", "daemon off;"]

Network Policies

Restrict ingress and egress traffic to only what the application requires:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: nginx-policy
spec:
  podSelector:
    matchLabels:
      app: nginx
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: ingress-controller
      ports:
        - port: 8080
          protocol: TCP
  egress: []

This policy:

  • Allows ingress only from pods labeled role: ingress-controller on port 8080
  • Blocks all egress traffic (nginx serving static content needs no outbound connections)
  • Denies all other ingress by default

Adjust egress rules if nginx is configured as a reverse proxy.

Pod Security Standards

ARMORRED images comply with the Kubernetes Pod Security Standards restricted profile. Enforce this at the namespace level:

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

The restricted profile requires:

RequirementARMORRED Compliance
runAsNonRoot: trueUID 65534 by default
allowPrivilegeEscalation: falseSupported
readOnlyRootFilesystem: trueDesigned for read-only root
capabilities.drop: ALLNo capabilities needed
seccompProfile: RuntimeDefaultCompatible

Resource Limits

Set appropriate resource requests and limits based on workload:

WorkloadCPU RequestCPU LimitMemory RequestMemory Limit
Static content50m200m32Mi64Mi
Reverse proxy100m500m64Mi128Mi
High traffic250m1000m128Mi256Mi

The reduced image size (121 MB hardened, 72 MB locked) means faster startup and lower baseline memory consumption compared to upstream (196 MB).

Complete Example

A production-ready deployment combining all security controls:


$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx created
service/nginx created
networkpolicy.networking.k8s.io/nginx-policy created

$ kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
nginx-7d8f9c6b5f-abc12 1/1 Running 0 30s
nginx-7d8f9c6b5f-def34 1/1 Running 0 30s

If the image does not include a shell, exec attempts will fail, preventing attackers from obtaining an interactive session even after container compromise:


$ kubectl exec -it nginx-7d8f9c6b5f-abc12 -- /bin/sh
error: unable to start container process: exec: "/bin/sh": \
stat /bin/sh: no such file or directory