← Back to blog
Cloud / AWSMay 2, 2026· 11 min

Container Security on AWS: ECR Scanning, ECS Hardening, and EKS Controls

A practitioner's walkthrough of locking down the container lifecycle on AWS, from Inspector-backed ECR scanning and image signing to ECS task-role isolation, IMDS exposure, EKS IRSA and Pod Identity, and GuardDuty Runtime Monitoring.

Container Security on AWS: ECR Scanning, ECS Hardening, and EKS Controls

Containers collapse three trust boundaries that used to be separate: the image supply chain, the workload identity, and the host. On AWS, ECR, ECS, and EKS each give you controls for one of those boundaries, but the defaults lean toward convenience. An image pushes whether or not it has been scanned, a task can inherit a wildly over-privileged instance role, and a pod can reach the node's instance metadata service and walk away with the node IAM credentials. This article walks the lifecycle end to end and shows the specific knobs that matter for working engineers in 2026.

ECR: enhanced scanning, provenance, and signing

ECR offers two scan tiers. Basic scanning runs the open-source Clair-derived engine on push and only covers OS packages. Enhanced scanning delegates to Amazon Inspector, which continuously rescans pushed images as new CVEs are published and, critically, reaches into language package layers: npm, PyPI, Go modules, Gem, Maven, and more. Continuous rescan is the differentiator. A clean scan at push time means nothing a week later when a new critical CVE drops against a library you already shipped. Turn on enhanced scanning at the registry level and set a scan-on-push plus continuous frequency so Inspector keeps evaluating images that are still in use.

  • Enable Inspector enhanced scanning at the registry scope, not per-repository, so new repos are covered by default.
  • Gate deployments on findings: query Inspector for CRITICAL/HIGH findings and fail the pipeline rather than trusting a stale push-time result.
  • Set image tag immutability on production repos so a tag like prod cannot be silently overwritten with a different digest.
  • Attach lifecycle policies to expire untagged and old images so vulnerable layers do not linger in the registry indefinitely.

Provenance is the other half. Pin deployments to image digests (sha256:...), never to mutable tags, so what you tested is byte-for-byte what runs. Sign images with AWS Signer container image signing (Notation, the CNCF Notary Project format) and verify signatures at admission. On EKS you can enforce verification with a Kyverno or Ratify admission policy that rejects any image lacking a trusted signature; on ECS, enforce it as a hard gate in the deploy pipeline before the task definition is registered.

ECS: the task-role vs instance-role boundary

The single most common ECS misconfiguration is conflating the container instance role with the task role. The instance role (or the Fargate-managed equivalent) exists so the ECS agent can talk to the control plane, pull images, and ship logs. Your application should never use it. Application permissions belong on the task role, which is scoped per task definition and delivered to the container through a credential endpoint the agent injects. If your app reads from one S3 bucket, the task role grants exactly that and nothing else, independent of what the host can do.

Hardening the task definition is equally important. Drop the privileged flag, run a read-only root filesystem, run as a non-root user, and drop Linux capabilities you do not need. The snippet below shows a hardened Fargate task definition with a dedicated task role, an execution role kept separate, a read-only root filesystem, and no privileged escalation.

{
  "family": "payments-api",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "512",
  "memory": "1024",
  "taskRoleArn": "arn:aws:iam::111122223333:role/payments-api-task",
  "executionRoleArn": "arn:aws:iam::111122223333:role/payments-api-exec",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/payments-api@sha256:9f2c...",
      "essential": true,
      "readonlyRootFilesystem": true,
      "privileged": false,
      "user": "10001:10001",
      "linuxParameters": {
        "capabilities": { "drop": ["ALL"] },
        "initProcessEnabled": true
      },
      "mountPoints": [
        { "sourceVolume": "tmp", "containerPath": "/tmp", "readOnly": false }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/payments-api",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "app"
        }
      }
    }
  ],
  "volumes": [ { "name": "tmp" } ]
}

A read-only root filesystem breaks code that expects to write to the image. The fix is not to disable the flag but to mount a small writable volume (here /tmp) for the few paths that genuinely need it. This blocks an attacker who lands code execution from dropping a persistent binary into the image filesystem.

ECS task access to instance metadata

On EC2-backed ECS, the most dangerous default is that a task on the host network, or any task that can reach 169.254.169.254, can query the node's instance metadata service and retrieve the container instance role credentials, bypassing the task-role scoping entirely. The controls are layered. Enforce IMDSv2 (session-token required) on the launch template so simple SSRF cannot grab credentials with a single GET. Set the IMDS hop limit to 1 so a packet originating inside a container, which adds a network hop, cannot reach metadata. And use awsvpc network mode rather than host so each task gets its own ENI and does not share the host's metadata path. On Fargate there is no host metadata endpoint to reach in the first place, which is one more reason to prefer it for sensitive workloads.

Rule of thumb: if a workload only needs AWS API permissions, it should get them from a task role or IRSA/Pod Identity, never from the node. Set the IMDS hop limit to 1 and require IMDSv2 everywhere, then treat any process reaching 169.254.169.254 from inside a container as an incident, not a feature.

EKS: IRSA, Pod Identity, and the IMDS hop limit for pods

EKS gives each pod its own AWS identity through one of two mechanisms. IRSA (IAM Roles for Service Accounts) federates the cluster's OIDC provider with IAM: the pod's projected service-account token is exchanged for role credentials, and the role's trust policy pins the exact namespace and service account allowed to assume it. EKS Pod Identity is the newer, simpler option that drops the per-cluster OIDC setup and manages the mapping through an EKS API and the Pod Identity Agent; prefer it for new clusters unless you need cross-account assumption patterns that IRSA still handles more directly. Either way, the goal is the same: the pod, not the node, holds least-privilege credentials. Below is an IRSA trust policy scoped to one service account so no other pod in the cluster can assume the role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE1A2B3C"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE1A2B3C:aud": "sts.amazonaws.com",
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE1A2B3C:sub": "system:serviceaccount:payments:payments-api"
        }
      }
    }
  ]
}

Pinning the sub claim to system:serviceaccount:payments:payments-api is what makes this least-privilege. A common mistake is matching only the aud claim or using a wildcard sub, which lets any pod in any namespace assume the role. As with ECS, set the IMDS hop limit on the EKS node group launch template to 1. Without that, a pod can still bypass IRSA entirely by reaching the node's metadata service and assuming the node instance role, which typically has the broad CNI, ECR, and worker permissions. Hop limit 1 closes that path because the pod's traffic crosses an extra hop and is dropped before it reaches metadata.

Kubernetes RBAC and runtime detection

AWS identity is only half of EKS authorization; Kubernetes RBAC governs what an identity can do inside the cluster. Bind subjects to namespaced Roles instead of cluster-wide ClusterRoles wherever possible, and never grant cluster-admin to CI or application service accounts. Audit for the dangerous verbs: a principal with escalate, bind, or impersonate can grant itself more than it appears to have, and broad pods/exec or secrets get is effectively node-level access. EKS access entries replaced the old aws-auth ConfigMap as the supported way to map IAM principals to Kubernetes groups, so manage cluster access through access entries and keep those group bindings tight.

  • Enable GuardDuty EKS Protection (audit-log analysis) plus Runtime Monitoring, which deploys an eBPF agent to ECS Fargate, EKS, and EC2 to flag process- and network-level threats like a container spawning a reverse shell or querying the metadata endpoint.
  • Turn on EKS control-plane audit logging to CloudWatch so RBAC denials and anomalous API calls are recorded for investigation.
  • Use a managed admission policy (Pod Security Admission at the restricted level, or Kyverno/Ratify) to reject privileged pods, hostPath mounts, and unsigned images at the door.
  • Scope network policy so pods cannot reach 169.254.169.254 or each other unless explicitly allowed, reinforcing the IMDS hop-limit control at the network layer.

None of these controls is exotic, but they only work as a set. Inspector scanning plus signing protects the image, task roles and IRSA/Pod Identity protect the workload identity, the IMDS hop limit and IMDSv2 protect the host boundary, and GuardDuty Runtime Monitoring plus RBAC catch what slips through. Configure them together and an attacker who compromises one container finds no node credentials to steal, no broader role to assume, and a detection agent watching every syscall.

Learn it by doing

Spin up a real AWS security lab, or explore our training tracks.

24 people viewing now