GuideJuly 2, 2026·13 min read
ecs-fargate-securityaws-fargate-security-best-practicescontainer-security-ecs-fargate

AWS ECS Fargate Security: What You Actually Configure (and What You Can't)

Every "Fargate security best practices" list is the same checklist — non-root, read-only filesystem, least-privilege IAM. The list isn't wrong; it's just written for one task. If you run 10–40 ECS environments, the hard part isn't setting these knobs — it's keeping them identical everywhere and proving it for SOC 2. This is Fargate security from the operator's seat: the exact config, what Fargate won't let you touch, and where fleets actually break.

Matt S
Matt S
Platform engineer at Fortem
TL;DR
  • ·Fargate splits security into two IAM roles — the EXECUTION role (agent: pull image, ship logs, fetch secrets) and the TASK role (your app code). Confusing them is the #1 config mistake.
  • ·The Fargate hardening set is small and fixed: non-root user, readonlyRootFilesystem, secrets via Secrets Manager/SSM (never plaintext env vars), awsvpc security groups per task. Ephemeral storage is already AES-256 encrypted.
  • ·Fargate REMOVES options on purpose: no privileged containers, no host access, no SSH, CAP_SYS_ADMIN/NET_ADMIN blocked — and you can't run your own runtime agent (Falco DaemonSet). GuardDuty's injected sidecar is the only runtime-threat path.
  • ·GuardDuty ECS Runtime Monitoring bills per monitored vCPU-hour and won't attach to an already-running task — a real cost and coverage gap at 10+ environments.
  • ·At fleet scale the risk isn't the config, it's DRIFT: one env with a too-broad task role, a staging secret nobody rotated, a public-subnet task in environment #39.
Ready to use — copy this today

A hardened task-definition fragment, the writable-/tmpmount that makes read-only root actually work, and a least-privilege task role scoped to one environment's resources — no wildcards:

json
// 1. Hardened container definition (the fields that matter for security)
{
  "name": "app",
  "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/app:1.4.2",
  "user": "1000:1000",                    // non-root — matches USER in Dockerfile
  "readonlyRootFilesystem": true,          // root FS read-only
  "privileged": false,                     // (not supported on Fargate anyway)
  "secrets": [                             // secret VALUES never live here
    {
      "name": "DB_PASSWORD",
      "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/db-AbCdEf"
    }
  ],
  "mountPoints": [
    { "sourceVolume": "tmp", "containerPath": "/tmp", "readOnly": false }
  ],
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": { "awslogs-group": "/ecs/app", "awslogs-region": "us-east-1", "awslogs-stream-prefix": "app" }
  }
}
json
// 2. The writable volume that keeps read-only root from breaking the app.
// Declared at the task level; mounted at /tmp above. Everything else stays RO.
"volumes": [
  { "name": "tmp", "host": {} }
]
json
// 3. Least-privilege TASK role — scoped to ONE environment's bucket, no wildcards.
// This is the role your app code uses. Give each environment its own.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::acme-prod-uploads/*"
    }
  ]
}

The two IAM roles Fargate security starts with

Fargate uses two IAM roles: the execution role lets the agent pull the image, ship logs, and fetch secrets; the task role is what your app code uses to call AWS. Mixing them up is the top mistake.

The execution role is used by the Fargate agent to set the task up before your container runs — pull the image from ECR, write logs, and inject any secrets the task definition references. The task role is used by your application at runtime to reach S3, DynamoDB, and other services. Secret injection and ECR pulls are execution-role permissions; attaching them to the task role is the single most common config error, and a recurring AWS re:Post question.

One thing worth internalizing: AWS is explicit that "containers are not a security boundary… each task running on Fargate has its own isolation boundary and does not share the underlying kernel, CPU, memory, or elastic network interface with another task." The isolation you rely on is the task, not the container. Scoping a separate least-privilege task role per environment is exactly the per-environment IAM isolation across the fleetthat keeps one compromised task from reaching another environment's data.

The Fargate hardening set (the config that matters)

The Fargate hardening set is short: run as a non-root user, set readonlyRootFilesystem, drop unneeded capabilities. Ephemeral storage is already AES-256 encrypted, so that box is checked for you.

By default a container runs as root unless your Dockerfile has a USERdirective. AWS's own guidance is to run as non-root and to lint Dockerfiles in CI, failing the build when the USERdirective is missing — that turns "we run non-root" from a hope into a gate.

A container's root filesystem is writable by default; you set readonlyRootFilesystem: true explicitly. Ephemeral storage — the 20 GiB scratch space (up to 200 GiB) each task gets — is encrypted with AES-256 using a Fargate-managed key for any task launched since May 2020, so encryption-at-rest for scratch data needs no action from you. The read-only setting is the one that bites people, which is the next section.

Making read-only root filesystem actually work

readonlyRootFilesystem: true breaks any app that writes to /tmp. The fix is a writable volume mounted at /tmp (and any other write path) while the rest of the filesystem stays read-only.

This is the friction point behind the glib "just make the filesystem read-only" advice. The moment you flip it on, an app that writes a session file, a cache, or a temp upload to /tmpstarts failing — and it fails as a confusing runtime crash, not as an obvious "permission denied on a read-only filesystem." It's a recurring ECS re:Post thread.

The fix is in the ready-to-use block above: declare an ephemeral volume at the task level and mount it at every path the app writes to (usually /tmp), keeping the rest of the root filesystem read-only. You end up declaring your writable surface explicitly, which is the whole security point — you now know exactly where the container can write.

Key insight
Read-only root isn't a toggle you flip and forget — it's a contract that forces you to declare every writable path. Test it before prod; a missing /tmp mount looks like a random app crash, not a security setting.

Secrets: the sanctioned path vs plaintext env vars

Never put secrets in a task definition's plaintext environment block. Reference them in the secrets block from Secrets Manager or SSM; the execution role fetches and injects them at launch.

Anything in the task definition's environment block is stored and shown in plaintext in the console and the API — a DB password there is visible to anyone with ecs:DescribeTaskDefinition. The secrets block instead holds a reference (an ARN); the execution role resolves it at launch and injects the value as an env var the app sees, without the value ever living in the task definition.

For that to work, the execution role needs secretsmanager:GetSecretValue (for Secrets Manager) or ssm:GetParameters (for Parameter Store), plus kms:Decryptif the secret uses a customer-managed key. Note it's the execution role, not the task role — the agent fetches the secret before your code runs.

Network isolation with awsvpc and security groups

Fargate only runs in awsvpc mode, so every task gets its own ENI and IP and can carry its own security group. Scope security groups per task and keep tasks in private subnets with no public IP.

awsvpcis the only network mode available on Fargate, and it's the mode that gives each task a dedicated elastic network interface with its own private IP. That means a security group attaches to the task itself — you can scope ingress and egress per service instead of sharing one host's rules across everything, the way bridge mode forces on EC2.

The operational rule: tasks go in private subnets, reach ECR and Secrets Manager over a NAT gateway or VPC endpoints, and never get an auto-assigned public IP. The failure that undoes all of this is a single environment stood up from an older module that drops the task in a public subnet with a permissive security group. It's easy to miss when environments are built one at a time — the fleet-drift problem below.

What Fargate won't let you configure (vs EC2)

Fargate removes low-level controls on purpose: no privileged containers, no host access, no SSH, CAP_SYS_ADMIN and CAP_NET_ADMIN restricted. The only capability you can add is CAP_SYS_PTRACE.

A lot of "container security" advice assumes host-level control you simply don't have on Fargate. Privileged containers aren't supported, so Docker-in-Docker patterns don't run. Additional Linux capabilities like CAP_SYS_ADMIN and CAP_NET_ADMIN are restricted to prevent privilege escalation; only CAP_SYS_PTRACEcan be added, for observability and security tooling inside the task. And there's no host access at all — ECS exec is the only sanctioned way to get a shell into a running container.

ControlEC2 launch typeFargate
Privileged containersAllowedNot supported
Docker-in-DockerPossibleNot possible
Host access / SSHYes (host + ECS exec)No host; ECS exec only
Custom runtime agent (Falco)DaemonSet on the hostNone — GuardDuty sidecar
Linux capabilitiesAdd most capsOnly CAP_SYS_PTRACE addable
Kernel modules / sysctlsHost-level controlLocked down
Read-only root filesystemConfigurableConfigurable
Non-root userConfigurableConfigurable

The bottom two rows are the ones you still own. Everything above them is decided for you — which is the point of Fargate. The consequence that surprises people: you can't run your own runtime sensor, so runtime threat detection works differently.

Runtime threat detection = GuardDuty (there's no alternative)

Because you can't run your own agent on Fargate, GuardDuty ECS Runtime Monitoring is the only managed runtime-threat path. It injects a sidecar into each task and bills per monitored vCPU-hour.

On EC2 you'd run a Falco DaemonSet or a vendor agent on the host to watch process execution, file access, and network connections at runtime. On Fargate there's no host to put it on. AWS closes that gap with GuardDuty ECS Runtime Monitoring: when a task starts, GuardDuty attaches a managed security sidecar container to it. AWS is explicit that on Fargate you cannot manage that agent manually — GuardDuty is the only supported runtime path.

Two operational gotchas. First, a Fargate task is immutable, so GuardDuty won't attach the sidecar to a task that's alreadyrunning — you stop and restart the task to bring it under monitoring. Second, it's billed per monitored vCPU-hour on a tiered rate (with a 30-day free trial), so across 10+ always-on environments it's a real, and often unpredictable, line item. Check the GuardDuty pricing pagefor your region's exact rate before you assume it's free.

Image supply chain — scanning and immutable tags

Secure the image before it runs: enable ECR scan-on-push, set tag immutability so a tag can't be silently overwritten, and fail the build on HIGH or CRITICAL CVEs. Basic scanning is free.

Runtime hardening only matters if the image itself is sound. ECR basic scanning is free, uses an AWS-native CVE database (the older Clair-based basic scanning was retired on October 1, 2025), and checks each image on push; enhanced scanning via Amazon Inspector goes deeper into OS and language packages. Tag immutability stops a second push of :v1.4.2 from silently replacing the bytes you already reviewed and deployed.

The registry is its own security surface — pull IAM, cross-account access, and lifecycle all matter at fleet scale. That's covered in depth in how ECR works for ECS Fargate teams; for security specifically, scan-on-push plus immutable tags is the baseline every repo should be born with.

Where fleets actually break — security drift at 10+ environments

At fleet scale the config is correct per-env but unaudited across envs. The real risks: one env with a too-broad task role, a secret rotated in prod but not staging, a public-subnet task missed.

Every control above is easy to set on one task. The problem is that a fleet of 10–40 environments is edited one task definition at a time, in isolation, and nothing shows you all of them at once. So the failures are failures of uniformity:

  • ·Task-role drift. Prod's role got scoped to least-privilege during the SOC 2 push; the six-month-old sandbox and demo-eu environments still carry a copy-pasted role with s3:* or a wildcard Resource. Nothing flags it, because each env's task def is edited on its own.
  • ·Secrets rotated unevenly. The DB credential is rotated in prod, but staging and qa still reference the old secret ARN — or worse, still carry the value in a plaintext environment var from before the migration. The secret is "handled" in one environment.
  • ·The public-subnet task nobody noticed. awsvpc and security groups are correct in 38 environments; #39 was stood up from an older Terraform module that drops the task in a public subnet with a permissive SG. It passes every single-env checklist, because that checklist only ever looks at one env.
  • ·Uneven read-only / non-root. readonlyRootFilesystem and the USER directive are enforced in the environments the platform team built, and skipped in the ones a product squad self-served. The hardening exists as a policy but not as a fact across the fleet.
  • ·GuardDuty on 30 of 40 clusters. Runtime Monitoring is on account-wide but excluded via tag on the clusters someone muted during a noisy-alert incident and never re-enabled. Ten environments have zero runtime detection, and no dashboard shows the gap.

Container security on Fargate is not hard to configure — it's hard to keep uniform and prove. The reframing from "is this task hardened?" to "is every environment hardened the same way?" is the whole job at 10+ environments, and it's the thing a per-task checklist structurally can't answer.

How this ties into your SOC 2 controls

SOC 2 doesn't ask if you CAN secure a task — it asks you to prove the control holds in every environment. Fleet-wide uniformity is the audit evidence, and it breaks when envs are configured by hand.

Each item in the hardening set maps to a control an auditor will ask about: least-privilege access (the task role), encryption (ephemeral storage and secrets), change monitoring (who edited a task definition). The gap auditors find isn't that you can'tdo these things — it's the one environment where the control isn't applied. Drift is the finding they circle in red, and proving uniformity across the fleet is the evidence that closes it.

If you read this, you might also want to know

Do I need GuardDuty if I already scan images in ECR?

Yes — they cover different phases. ECR scanning finds known CVEs in the image before it runs (build-time / supply chain). GuardDuty Runtime Monitoring watches behavior while the container runs — process execution, file access, outbound connections — and catches things a clean image can still do at runtime, like credential theft or crypto-mining. Scanning is prevention; runtime monitoring is detection.

Can I run Falco or my own security agent on Fargate?

No. Falco and similar tools need host-level access (a DaemonSet or kernel module) that Fargate doesn't give you — there's no host to install them on. GuardDuty ECS Runtime Monitoring, which injects an AWS-managed sidecar, is the supported substitute. If a vendor claims Fargate runtime coverage, they're either using GuardDuty's feed or running in-task with the limited CAP_SYS_PTRACE capability.

Is the task role or the execution role the one that reads my secrets?

The execution role. The Fargate agent uses the execution role to fetch a secret referenced in the task definition's secrets block and inject it before your container starts, so the execution role needs secretsmanager:GetSecretValue or ssm:GetParameters. The task role is only for AWS calls your application makes at runtime.

FAQ

Securing ECS across many environments?

Every environment's security config,
on one screen.

Fortem maps every ECS environment — the task role each one carries, whether secrets are referenced or plaintext, which subnet and security group it lands in, and where GuardDuty coverage has a gap. Book a 20-minute walkthrough.

Worth reading