Deep dive
Decision table
| Workload | Boundary | Why |
|---|---|---|
| Your own services, trusted code | Container, non-root, read-only rootfs, dropped caps, seccomp default | Cheap, contains most exploits |
| Parsing untrusted files (images, PDFs, video) | Separate process/container with no network, low memory limit, seccomp | Parsers are the classic RCE entry point |
| Running customer or AI-generated code | Firecracker/Kata microVM or gVisor; no metadata, egress allowlist | Kernel exploits escape containers |
| Plugins inside your process | WebAssembly (Wasmtime) with explicit host functions | Capability-based, memory isolated |
| Prod vs staging vs dev | Separate cloud accounts/projects, separate credentials | A dev mistake can't touch prod |
| Multi-tenant data | Per-tenant keys or RLS + per-tenant authz checks | Bugs leak one tenant, not all |
| Humans | SSO, short sessions, just-in-time elevation, break-glass account audited | No standing admin rights |
| Database access | Role per service with only needed tables/verbs; migrations under a separate role | SQL injection can't DROP or read other data |
Code
systemd service hardening (check with systemd-analyze security app.service):
[Service]
ExecStart=/usr/local/bin/app
DynamicUser=yes # ephemeral unprivileged user
LoadCredential=db_password:/etc/app/db_password
NoNewPrivileges=yes
ProtectSystem=strict # / read-only
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
StateDirectory=app # the only writable path: /var/lib/app
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes # remove for JIT runtimes (JVM, V8, .NET)
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
CapabilityBoundingSet= # no capabilities; add CAP_NET_BIND_SERVICE for ports < 1024
Kubernetes pod security context:
securityContext: # pod level
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: app
image: ghcr.io/org/app@sha256:<digest>
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
resources:
limits: { memory: "512Mi" }
automountServiceAccountToken: false # unless the pod calls the K8s API
Enforce cluster-wide with Pod Security Admission restricted, plus a default-deny NetworkPolicy per namespace.
Scoped AWS IAM policy (one bucket prefix, read/write, nothing else):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::acme-uploads/tenant-data/*"
}]
}
Postgres role per service:
CREATE ROLE orders_svc LOGIN;
GRANT SELECT, INSERT, UPDATE ON orders, order_items TO orders_svc;
GRANT USAGE ON SEQUENCE orders_id_seq TO orders_svc;
-- no DELETE, no DDL, no access to users table
Efficiency
Cost: namespaces, cgroups, capability drops, read-only filesystems, scoped IAM policies and per-service database roles cost nothing at runtime; the price of isolation is paid at the boundary (syscalls, VM exits, startup time, memory per sandbox).
Levers:
- Cheapest boundary for the threat — hardened containers for trusted code, VM-class sandbox only for untrusted code (decision table) — always choose first.
- Pool or snapshot sandboxes — Firecracker states boot under 125 ms, under 5 MiB overhead per microVM, up to 150 microVMs created per second per host (project site; Agache et al.) — avoids a boot per request.
- Wasm in-process — Wasmtime avoids a process or VM crossing per call; its pooling allocator speeds instance creation — plugins.
- gVisor — intercepts syscalls in user space, so syscall- and network-heavy workloads pay most; measure first — untrusted code with light I/O.
- Short seccomp filters — group allowlists such as
@system-service— any container.
Naive → standard → state of the art:
- Naive: shared root account, all-powerful containers · zero overhead, no isolation
- Standard: one account per environment plus non-root, read-only, dropped-capability containers · negligible overhead
- State of the art: pooled or snapshotted microVMs or Wasm for untrusted code · order of ms to start, MiB per sandbox
Implementation notes
- Start from deny-all and add permissions from observed failures in staging; tools like IAM Access Analyzer generate policies from real usage logs.
- Egress allowlists matter as much as ingress: most exfiltration and command-and-control needs outbound connections.
- Separate the deploy identity (can change infrastructure) from the runtime identity (can only use it).
- Break-glass admin accounts: stored offline, MFA, alert on every use.
- Re-check privileges regularly; permissions only accumulate unless someone removes them.
Pitfalls
- Mounting
/var/run/docker.sockinto a container: equivalent to root on the host. privileged: trueorhostNetwork/hostPID"to make it work".- Container runs as root because the base image defaults to it; set
USERin the Dockerfile. - IAM
Action: "*"orResource: "*"granted during development and never narrowed. Use access analyzers to generate policies from real usage. - SSRF to
169.254.169.254stealing the node's cloud credentials; require IMDSv2 with hop limit 1, or block metadata via network policy. - One shared service account for all services: no blast-radius boundary.
- Kubernetes default service account tokens mounted in every pod.
- CI runners with production admin credentials shared across all repos.
- Separate VMs that share the same SSH key or the same database superuser.
- Standing SSH access to production for debugging instead of audited, time-boxed sessions (SSM Session Manager, Teleport).
- Analytics and BI tools reusing the application's database credentials, with write access they never need.
In production
- Cloud infrastructure: AWS Lambda runs on Firecracker microVMs (project site).
- Consumer software: the Chromium sandbox applies least privilege to sandboxed and to controlling code (design doc).
- Government: NIST SP 800-53 Rev. 5 control AC-6 requires least privilege for federal information systems (NIST).
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Firecracker | Rust | firecracker-microvm/firecracker | download a release binary from GitHub releases | — | — | — | — | microVMs on KVM; AWS Lambda and Fargate |
| Wasmtime | Rust | bytecodealliance/wasmtime | cargo add wasmtime / brew install wasmtime | — | — | — | — | WebAssembly runtime, capability-based WASI |
| landlock | Rust | landlock-lsm/rust-landlock | cargo add landlock | — | — | — | — | unprivileged self-sandboxing of filesystem access (Linux) |
| gVisor | Go | google/gvisor | runsc, see install guide | — | — | — | — | user-space kernel; OCI runtime for Docker and Kubernetes |
| systemd | C | systemd/systemd | n/a (ships with most Linux distributions) | — | — | — | — | systemd-analyze security scores unit hardening |
Sources
- Saltzer, Schroeder, The Protection of Information in Computer Systems, Proceedings of the IEEE, 1975. paper
- Agache et al., Firecracker: Lightweight Virtualization for Serverless Applications, NSDI 2020. paper · code
- systemd, systemd.exec(5) manual. doc
- Kubernetes documentation, Pod Security Standards. doc
- AWS, IAM User Guide: Security best practices in IAM. doc