How to Copy Files from S3 to EC2 Using AWS CLI: The Exact Command & IAM Setup

A common operational task — pulling a config file, a deployment artifact, or a secrets template from S3 onto a running EC2 instance — trips up engineers not because the command is complex, but because a missing IAM permission silently blocks the transfer. This guide gives you the exact aws s3 cp command, the minimal IAM policy required, and the mental model to debug it when it fails.

TL;DR

StepWhat You DoKey Detail
1Create an IAM Role with S3 read permissionAttach to EC2 instance profile
2Attach the role to your EC2 instanceNo access keys needed on the instance
3SSH into EC2, run aws s3 cpAWS CLI uses instance metadata credentials automatically

Architecture: How the Credential Flow Works

Before running any command, understand how the EC2 instance authenticates to S3. It does not use hardcoded access keys. Instead, it uses an IAM Instance Profile — a container that holds an IAM Role — which the EC2 metadata service (IMDS) exposes as temporary credentials to the AWS CLI automatically.

sequenceDiagram participant CLI as "AWS CLI
(on EC2)" participant IMDS as "EC2 Metadata Service
(169.254.169.254)" participant STS as "AWS STS" participant IAM as "IAM Policy Evaluator" participant S3 as "Amazon S3" CLI->>IMDS: "GET /latest/meta-data/iam/security-credentials/EC2S3ReadConfigRole" IMDS-->>CLI: "Temporary credentials (AccessKeyId, SecretKey, SessionToken)" CLI->>S3: "GetObject request (signed with temp credentials)" S3->>IAM: "Evaluate: Does this role allow s3:GetObject on this resource?" IAM-->>S3: "Allow" S3-->>CLI: "Object data (file content)"
  1. EC2 Instance runs the aws s3 cp command.
  2. The AWS CLI on the instance calls the EC2 Instance Metadata Service (IMDS) at http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> to retrieve temporary STS credentials.
  3. The CLI uses those credentials to make an authenticated S3 API call (GetObject).
  4. S3 evaluates the request against the IAM Role's policy and returns the file if authorized.
Analogy: Think of the IAM Instance Profile as a hotel key card programmed at check-in. The EC2 instance (guest) doesn't carry a master password — it just presents the card (temporary credentials from IMDS) at the door (S3 API), and the hotel's access system (IAM) decides which rooms it can open.

Step 1: Create the IAM Role & Policy

Apply the principle of least privilege. If the EC2 instance only needs to read a specific file or bucket prefix, scope the policy accordingly. Do not grant s3:* or attach AmazonS3FullAccess.

Minimal IAM Policy (Read a Specific Prefix)

🔽 Click to expand — IAM Policy JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3GetObjectForConfig",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::my-app-config-bucket/configs/*"
    },
    {
      "Sid": "AllowS3ListBucketForConfig",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::my-app-config-bucket",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["configs/*"]
        }
      }
    }
  ]
}

Why two statements? s3:GetObject operates on objects (ARN includes the key path). s3:ListBucket operates on the bucket itself (ARN is just the bucket). These are separate IAM resource types and must be declared separately. The aws s3 cp command for a single file only strictly requires s3:GetObject, but s3:ListBucket is needed if you use wildcards or the aws s3 sync command.

Trust Policy for the Role (EC2 Service Principal)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Create the Role via AWS CLI

# 1. Create the IAM role with the trust policy
aws iam create-role \
  --role-name EC2S3ReadConfigRole \
  --assume-role-policy-document file://trust-policy.json

# 2. Create and attach the inline permission policy
aws iam put-role-policy \
  --role-name EC2S3ReadConfigRole \
  --policy-name S3ReadConfigPolicy \
  --policy-document file://s3-read-policy.json

# 3. Create the instance profile and add the role to it
aws iam create-instance-profile \
  --instance-profile-name EC2S3ReadConfigProfile

aws iam add-role-to-instance-profile \
  --instance-profile-name EC2S3ReadConfigProfile \
  --role-name EC2S3ReadConfigRole

Step 2: Attach the Instance Profile to Your EC2 Instance

You can attach an instance profile at launch or to a running instance.

# Attach to a RUNNING instance (replace i-0123456789abcdef0 with your Instance ID)
aws ec2 associate-iam-instance-profile \
  --instance-id i-0123456789abcdef0 \
  --iam-instance-profile Name=EC2S3ReadConfigProfile \
  --region us-east-1

Verify the association:

aws ec2 describe-iam-instance-profile-associations \
  --filters Name=instance-id,Values=i-0123456789abcdef0 \
  --region us-east-1

Step 3: Run the aws s3 cp Command on EC2

SSH into your EC2 instance. The AWS CLI should already be installed on Amazon Linux 2 / Amazon Linux 2023 AMIs. For Ubuntu, install it via the official AWS installer.

Copy a Single File from S3 to EC2

# Syntax
aws s3 cp s3://<bucket-name>/<key> <local-destination-path>

# Real example: download app.config from S3 to /etc/myapp/
aws s3 cp s3://my-app-config-bucket/configs/app.config /etc/myapp/app.config

Copy a Directory Recursively from S3 to EC2

# Use --recursive flag to copy all objects under a prefix
aws s3 cp s3://my-app-config-bucket/configs/ /etc/myapp/ --recursive

Useful Flags

FlagPurposeExample
--recursiveCopy all objects under a prefixaws s3 cp s3://bucket/prefix/ /local/ --recursive
--regionSpecify bucket region explicitly--region us-west-2
--sseSpecify server-side encryption type on upload--sse aws:kms
--no-progressSuppress progress output (useful in scripts)--no-progress

Step 4: Verify Credentials Are Working (Debugging)

If the command fails with An error occurred (AccessDenied), run these checks in order:

graph TD A["aws s3 cp fails"] --> B{"Is an IAM Role attached to EC2?"} B -- "No" --> C["Attach Instance Profile to EC2 instance"] B -- "Yes" --> D{"Does aws sts get-caller-identity work?"} D -- "No" --> E["Check IMDS accessibility IMDSv2 token required?"] D -- "Yes" --> F{"Does IAM Role policy allow s3:GetObject on this resource ARN?"} F -- "No" --> G["Fix IAM policy Resource ARN to match bucket/key"] F -- "Yes" --> H{"Is there a Bucket Policy explicitly denying access?"} H -- "Yes" --> I["Update S3 Bucket Policy to allow role ARN"] H -- "No" --> J{"Is object encrypted with KMS CMK?"} J -- "Yes" --> K["Add kms:Decrypt permission to IAM Role"] J -- "No" --> L["Enable CloudTrail S3 data events for deeper audit"] style A fill:#ff6b6b,color:#fff style C fill:#51cf66,color:#fff style G fill:#51cf66,color:#fff style I fill:#51cf66,color:#fff style K fill:#51cf66,color:#fff
  1. Check if the instance has a role attached: curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ — this should return the role name.
  2. Check the active credentials: aws sts get-caller-identity — confirms which role the CLI is using.
  3. Verify the IAM policy resource ARN matches the exact bucket name and key prefix.
  4. Check for a bucket policy on the S3 side that may explicitly deny access, which overrides IAM allow statements.
  5. Check for KMS encryption: If the S3 object is encrypted with a customer-managed KMS key, the IAM role also needs kms:Decrypt permission on that key.

Special Case: Cross-Account S3 Access

If the S3 bucket is in a different AWS account than the EC2 instance, you need both:

  • The IAM Role on the EC2 account must have s3:GetObject permission.
  • The S3 Bucket Policy in the target account must explicitly grant access to the EC2 role's ARN (e.g., arn:aws:iam::123456789012:role/EC2S3ReadConfigRole).

Without the bucket policy grant in the target account, the request will be denied even if the IAM role policy allows it.

Glossary

TermDefinition
IAM Instance ProfileA container that passes an IAM Role to an EC2 instance, enabling it to call AWS APIs without static credentials.
IMDS (Instance Metadata Service)An EC2-internal HTTP endpoint (169.254.169.254) that provides instance metadata including temporary IAM role credentials.
s3:GetObjectThe IAM action required to download (read) an object from an S3 bucket.
Resource ARN ScopingRestricting an IAM policy's Resource field to a specific bucket or prefix instead of *, following least privilege.
Bucket PolicyA resource-based policy attached directly to an S3 bucket that can grant or deny access independent of IAM identity policies.

Next Steps

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

EC2 SSH Connection Timeout: The Exact Security Group Rules You Need to Fix It

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User