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
Post a Comment