Skip to content
Hosting Operations11 min read

Serverless vs Containers: 2026 Security Hardening

Secure serverless and container deployments with threat modeling, audit checklists, and hardening steps that reduce attack surface.

Written by Abdul AbrorTechnical Hosting Support Engineer
red and black plastic crates
On this page

TL;DR — Key takeaways

  • Serverless functions need strict IAM policies per function, not account-wide admin roles, to limit blast radius during breaches.
  • Container images must be scanned for CVEs before deployment and run as non-root users with read-only filesystems when possible.
  • Both architectures require network segmentation, secrets management outside environment variables, and audit logging enabled at the platform level.
  • Cost security matters too: unthrottled serverless functions can drain budgets during DDoS or runaway loops; set concurrency limits and billing alerts.

Security architecture differs between serverless and containers. Not in theory, but in the specific controls you configure and the attack surfaces that matter. I have seen production breaches start from an overpermissioned Lambda function and from a container running as root with an unpatched base image. Both are preventable.

This guide covers the threat model for each architecture, the audit checklist you should run before deploying, and the hardening steps that reduce risk. It also includes verification commands so you can confirm your configuration is actually secure, not just assumed to be.

Threat Model: What Actually Gets Compromised

Start by understanding what an attacker gains when they compromise your workload. In serverless, the function runtime itself is ephemeral and isolated. The persistent risk is the IAM role attached to the function. If that role has broad permissions, one exploit gives the attacker access to your entire cloud account.

I have handled incidents where a single vulnerable API endpoint running in Lambda had AdministratorAccess. The attacker used it to enumerate S3 buckets, exfiltrate data, and create backdoor users. The function itself was patched within hours, but the IAM role had been there for months.

Containers have a different profile. The runtime persists longer, so an attacker can install malware, escalate privileges if running as root, and pivot to other containers on the same node. The blast radius depends on your orchestration setup. Containers sharing a host kernel can exploit kernel vulnerabilities to break out. Separate VMs or isolated node pools contain the damage.

Secrets management is a shared risk. Both architectures often store API keys and database credentials in environment variables, which are visible in logs, crash dumps, and process listings. An attacker who gains read access to /proc or cloud console metadata can harvest every secret.

Audit Checklist: What to Check Before Deploying

Run these checks on every serverless function and container deployment. Automate them in CI/CD if possible, because manual reviews miss things during busy weeks.

  • IAM or role permissions: does the function or service account have more than it needs? Look for wildcard actions, overly broad resource ARNs, and administrator policies. Use the principle of least privilege by default.
  • Image vulnerabilities: scan container base images with tools like Trivy, Grype, or your registry's built-in scanner. Fail the build if high or critical CVEs are present. For serverless, check dependency vulnerabilities in your package manifest.
  • Network exposure: is the function or container accessible from the public internet? If it only serves internal requests, place it in a private subnet and use service endpoints or VPC links.
  • Logging and monitoring: are audit logs enabled? Can you see who invoked the function, what permissions were used, and whether the container spawned unexpected processes? If not, you cannot detect breaches.
  • Secrets storage: are secrets in environment variables? Move them to a managed vault like AWS Secrets Manager, Google Secret Manager, or HashiCorp Vault. Rotate them automatically.
  • Resource limits: are there memory, timeout, and concurrency caps? Unlimited functions can be abused for cryptomining or DDoS amplification. Unlimited containers can exhaust node resources and crash unrelated workloads.
  • Runtime protection: for containers, are AppArmor, SELinux, or seccomp profiles applied? For serverless, does the platform support runtime monitoring or function-level firewalls?

Hardening Serverless Functions

Serverless security starts with IAM. Create a unique execution role for each function instead of sharing one role across all functions. Each role should only include the specific actions and resources that function needs. For example, a function that writes logs to CloudWatch and reads from one DynamoDB table gets logs:PutLogEvents and dynamodb:GetItem on that table ARN only.

Check existing roles with your cloud provider's access analyzer. AWS IAM Access Analyzer shows unused permissions over the last 90 days. Remove anything the function has not touched. If a function is granted s3:* but only ever called GetObject on one bucket, narrow the policy.

Set memory and timeout limits. A function that normally completes in 3 seconds should have a 10-second timeout, not the default 15 minutes. Memory should match the actual usage with a small buffer. This prevents runaway executions from draining your budget or being used for abuse.

Concurrency limits prevent cost-based DDoS. An attacker who triggers 10,000 function invocations per second can generate a five-figure bill in an hour. Set reserved or provisioned concurrency to a value your infrastructure can handle and your budget allows. Most legitimate workloads do not need unlimited burst capacity.

Move secrets out of environment variables. Use the secrets manager integration provided by your platform, and grant the function permission to read only the specific secret it needs. Rotate secrets automatically on a schedule. I recommend 30 or 60 days for API keys and database passwords.

Enable VPC mode for functions that access internal resources. Place the function in a private subnet with no internet gateway, and use VPC endpoints or NAT gateways for outbound access. This stops attackers from using a compromised function to scan or attack the public internet.

Hardening Containers

Run containers as non-root. Add a USER directive in your Dockerfile after installing dependencies but before setting the entrypoint. Pick a high UID like 10000 to avoid conflicts with system users. Test thoroughly because some applications expect to write to directories like /tmp or /var/log, which require group write permissions when running as non-root.

Use read-only root filesystems where possible. Set readOnlyRootFilesystem: true in your pod or task definition. If your application needs to write temporary files, mount an emptyDir or tmpfs volume at /tmp. This prevents attackers from installing malware or modifying binaries after exploiting the application.

Scan images before deployment. Integrate Trivy, Grype, or Anchore into your CI pipeline and fail the build if critical vulnerabilities are detected. Update base images regularly, not just when a breach happens. Many breaches exploit CVEs that were public for months.

Drop unnecessary capabilities. Containers start with a default set of Linux capabilities that most applications never use. Explicitly drop all and add back only what you need, usually just CHOWN, SETUID, and SETGID for typical web applications. CAP_SYS_ADMIN, CAP_NET_ADMIN, and CAP_SYS_PTRACE are almost never required and are dangerous in attacker hands.

Apply security profiles. Use AppArmor or SELinux profiles to restrict what system calls the container can make. Kubernetes supports seccomp profiles that block dangerous syscalls. The default runtime/default profile is better than nothing; custom profiles tailored to your application are better still.

Segment the network. Use network policies to block traffic between pods or containers that do not need to communicate. Default deny all traffic, then allow specific sources and destinations. This limits lateral movement if one container is compromised.

Cost Security: Preventing Budget Drain

Security is not just about data breaches. An attacker can abuse your infrastructure to generate cost without stealing anything. Serverless functions are vulnerable to invocation floods; containers are vulnerable to resource exhaustion.

For serverless, set concurrency limits and billing alerts. A function with unlimited concurrency can be triggered millions of times by an attacker, generating a massive bill even if each invocation is cheap. Use reserved concurrency to cap the maximum number of concurrent executions. Most applications do not need more than 100-500 concurrent invocations per function.

Containers need CPU and memory limits. Without them, a single compromised container can consume all node resources and crash everything on that host. Set requests to match typical usage and limits to a safe upper bound. A container that normally uses 256MB should have a request of 256MB and a limit of 512MB.

Monitor your spending. Set up alerts for unusual spikes in compute, storage, or data transfer costs. Attackers often test the waters with a small spike to see if you notice before launching a full-scale resource drain. Early detection saves money.

Verifying Your Configuration Is Secure

Run these verification tests after every major configuration change and at least quarterly for production systems. Drift happens. A teammate might disable a control to troubleshoot an issue and forget to re-enable it.

  • IAM permissions: try invoking the function with an action it should not have. If a function should only read from S3, try writing to S3 from within the function code. The request should fail with an access denied error.
  • Non-root containers: exec into a running container and check the user with id. The UID should match what you set in the Dockerfile, not 0 (root). Try writing to the root filesystem; it should fail if read-only mode is enabled.
  • Network segmentation: from a container or function in one security group or namespace, try connecting to a resource in another group that should be blocked. Use curl or nc to test. The connection should time out or be refused.
  • Secrets exposure: check environment variables with printenv or by reading /proc/1/environ. Secrets should not appear there. Verify that secrets are fetched from the vault at runtime by checking application logs or tracing API calls.
  • Image scanning: pull the image you deployed and scan it locally with trivy image <image-name>. The output should show zero high or critical vulnerabilities. If it shows any, the image was deployed before scanning was enforced.
  • Concurrency limits: trigger a burst of function invocations that exceeds your concurrency limit. Excess invocations should be throttled, not executed.
  • Logging: trigger an operation that should generate an audit log entry (e.g., function invocation, container start). Verify the log appears in your centralized logging system within one minute.

When Hardening Breaks Your Application

Strict security controls sometimes conflict with application behavior. Dropping Linux capabilities can prevent a legitimate binary from binding to a privileged port. Read-only filesystems break applications that expect to write to directories outside /tmp.

Document these conflicts and make intentional exceptions. If an application must run as root, isolate it in a dedicated node pool or use a VM-based isolation boundary instead of shared container nodes. If a serverless function needs broad permissions, split it into multiple single-purpose functions with narrow IAM roles.

Test in staging first. Apply hardening to a non-production environment and run your full test suite. Monitor for permission errors, file write failures, and network timeouts. Fix incompatibilities before deploying to production.

Roll back safely. Keep the previous configuration in version control so you can revert quickly if a hardened deployment causes an outage. Gradual rollouts are safer than flipping every control at once.

Quick troubleshooting checklist

  • Enable audit logging at the platform level (CloudTrail, Cloud Logging, or equivalent)
  • Scan container images for vulnerabilities before pushing to production registries
  • Remove default admin permissions from serverless function execution roles
  • Set memory and timeout limits on all serverless functions to prevent resource exhaustion
  • Configure network policies to block unnecessary egress traffic from containers
  • Store secrets in managed vaults, not environment variables or config files
  • Enable runtime protection or AppArmor/SELinux profiles for container workloads
  • Set concurrency limits and billing alerts to prevent cost-based attacks
  • Use private subnets and service endpoints to avoid exposing functions or containers to the public internet
  • Review IAM policies quarterly and remove unused permissions

FAQ

Which is more secure by default, serverless or containers?

Neither is secure by default. Serverless functions often ship with overly permissive IAM roles that grant access to entire cloud accounts. Containers ship with root user access and pull base images that may contain unpatched vulnerabilities. Both require explicit hardening: least-privilege IAM for serverless, non-root users and scanned images for containers.

How do I know if my serverless functions have too many permissions?

Check the IAM role attached to each function. If it includes wildcard actions like s3:* or administrator access policies, it is overpermissioned. Use cloud provider access analyzers to see what permissions the function actually used in the last 90 days, then remove everything else. A function that reads from one S3 bucket should only have GetObject on that specific bucket ARN.

What is the biggest container security mistake that causes breaches?

Running containers as root user. When an attacker exploits a vulnerability in your application code, root access lets them write to the filesystem, install malware, and pivot to other containers on the same node. Always set a non-root user in your Dockerfile with USER directives and test that the application still works before deploying.