Spring Boot on AWS EC2: Upload to S3 Securely Using IAM Role

When running a Spring Boot application on AWS EC2, the most common security mistake is hardcoding AWS access keys inside application.properties. This is unnecessary and risky. AWS already provides a secure mechanism: IAM Roles attached to EC2.

In this blog, you will learn how to upload and download files to Amazon S3 from Spring Boot running on EC2 without any access keys, using:

  • Spring Profiles
  • AWS SDK v2
  • Instance Metadata Service (IMDS)
  • IAM Roles

Why Access Keys Are a Bad Idea on EC2

Typical S3 setups use:

  • Access key
  • Secret key
  • Region
  • Bucket

This works only for local development. It should NOT be used on EC2.

Problems with access keys:

  • Stored in config or code (high risk)
  • Might leak through Git, logs, or screenshots
  • Require manual rotation
  • Not needed if app runs on EC2

AWS automatically manages temporary credentials using the EC2 Instance Profile (IAM Role).


Architecture: EC2 → IAM Role → S3

  • Spring Boot app runs inside EC2
  • EC2 has an attached IAM Role with S3 permissions
  • AWS SDK automatically fetches credentials from metadata
  • No accessKey/secretKey required in EC2 profile

Step 1 — Spring Profiles Setup

application-local.properties


cloud.aws.region=ap-south-1
cloud.aws.s3.bucket=my-app-bucket

cloud.aws.credentials.accessKey=YOUR_LOCAL_ACCESS_KEY
cloud.aws.credentials.secretKey=YOUR_LOCAL_SECRET_KEY

application-dev.properties (EC2)


cloud.aws.region=ap-south-1
cloud.aws.s3.bucket=my-app-bucket

Notice: No accessKey or secretKey in EC2 profile.


Step 2 — S3Config (Local vs EC2)


@Configuration
public class S3Config {

    @Value("${cloud.aws.region}")
    private String region;

    @Bean
    @Profile("local")
    public S3Client s3ClientLocal(
            @Value("${cloud.aws.credentials.accessKey}") String accessKey,
            @Value("${cloud.aws.credentials.secretKey}") String secretKey) {

        AwsBasicCredentials creds = AwsBasicCredentials.create(accessKey, secretKey);

        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(StaticCredentialsProvider.create(creds))
                .build();
    }

    @Bean
    @Profile("dev")
    public S3Client s3ClientEc2() {

        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build();
    }
}

✔ Local → Uses static keys
✔ EC2 → Uses IAM Role automatically


Step 3 — S3 Upload & Download Service


@Service
public class S3Service {

    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    @Autowired
    private S3Client s3Client;

    public String uploadFile(MultipartFile file) throws IOException {

        PutObjectRequest request = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(file.getOriginalFilename())
                .contentType(file.getContentType())
                .build();

        s3Client.putObject(request,
                RequestBody.fromBytes(file.getBytes()));

        return "Uploaded: " + file.getOriginalFilename();
    }

    public byte[] downloadFile(String fileName) {

        GetObjectRequest request = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .build();

        return s3Client.getObjectAsBytes(request).asByteArray();
    }
}

Step 4 — Controller for Upload & Download


@RestController
@RequestMapping("/s3")
public class S3Controller {

    @Autowired
    private S3Service s3Service;

    @PostMapping("/upload")
    public ResponseEntity upload(@RequestParam("file") MultipartFile file)
            throws IOException {
        return ResponseEntity.ok(s3Service.uploadFile(file));
    }

    @GetMapping("/download")
    public ResponseEntity download(@RequestParam String fileName) {
        byte[] bytes = s3Service.downloadFile(fileName);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName)
                .body(bytes);
    }
}

Step 5 — Create IAM Role & Attach to EC2

  • Go to IAM → Roles → Create Role
  • Trusted entity: AWS Service
  • Use case: EC2
  • Attach policy: AmazonS3FullAccess (or least-privilege)
  • Name: EC2S3Role
  • Attach to EC2 under: Actions → Security → Modify IAM Role

Step 6 — Build & Upload JAR to EC2


mvn clean package

Upload JAR to EC2:


scp -i key.pem target/app.jar ec2-user@PUBLIC_DNS:/home/ec2-user

Step 7 — Run App on EC2 Using dev Profile


nohup java -Dspring.profiles.active=dev -jar app.jar > app.log 2>&1 &

Check logs:


tail -f app.log

Step 8 — Test Upload & Download

Upload API (POST)


http://EC2-PUBLIC-IP:8080/s3/upload

Download API (GET)


http://EC2-PUBLIC-IP:8080/s3/download?fileName=test.jpg

If the IAM role is attached properly, both operations will work without any access keys.


Why This Is the Best Practice

  • No AWS keys in code or config
  • Temporary credentials auto-rotated
  • Least privilege access via IAM policy
  • Same code works locally & on EC2 using profiles
  • Clean, secure, production-ready AWS architecture

Comments

Popular posts from this blog

Java Streams Intermediate Operations Explained with Examples

ConcurrentHashMap vs Synchronized HashMap in Java