본문 바로가기

Infra/AWS

[AWS/Spring Boot] CloudFront Signed Url로 S3 버킷에 제한적으로 접근하는 Java 애플리케이션 구현하기

Why CloudFront + SignedUrl?

CloudFront는 온프레미스에서의 cdn 역할을 하는 AWS의 서비스이다. 원본 리소스는 S3 버킷에 저장되며 다수의 Edge에 리소스를 배포하고 동시에 다수의 사용자가 접근할 때 가장 가까운 Edge에서 리소스를 반환해준다.

CloudFront는 Http또는 RTMP를 통해 리소스에 대한 접근을 지원한다. 이때, 특정 자원은 특정 사용자의 접근만을 허용할 요구가 있을 수 있다. 그때 사용하는 것이 SingedUrl이다.

SingedUrl은 Url을 생성하는 사용자가 해당 Origin에 접근할 수 있는 권한이 있는지 체크하고 그 사용자의 서명을 Url의 파라미터에 추가한 것을 말한다. 따라서 퍼블릭하게 열려있는 CloudFront의 자원에 대한 접근을 제한할 수 있다. 이때, 추가적으로 Url 만료시간, 접근 Ip 대역폭 등 추가 옵셥을 제한할 수 있다.

++ SingedUrl의 단점은 Url이 항상 변하고, 서명된 Url이 노출된 경우에는 접근을 제한할 수 없다는 것이다. 이에 대한 해결책으로 나오는 것이 SingedCookie인데, 다음 포스팅에서 다루려고 한다.

기본세팅

CloudFront Key Pair 생성

CloudFront KeyPair는 루트계정으로만 발급 가능하다

SecretAccess KeyPair파일을 잘 보관한다.

S3 Setting

1. S3 버킷 생성
  • 퍼블릭 접근을 막기 위해 아래 설정을 체크한다.

2. 객체 업로드 - 퍼블릭 설정 수행하지 않고

CloudFront Setting

1. Web 배포 선택(Http/Https)

2. Create Distribution

Origin Settings

Origin Domain Name: CF를 통해 불러올 Origin을 선택한다. 여기에서는 S3 버킷이 해당된다.
Origin Path: Domain 아래 특정 디렉터리와 같이 Path를 특정할 필요가 있을 때 /로 시작하는 Path를 입력한다. 여기에서는 버킷 아래 바로 있는 객체를 불러올 것이기 때문에 비워둔다.
Origin Id: Description of Origin
Restrict Bucket Access: 버킷에 대한 접근을 제한한다. Signed Url을 발급함으로써 버킷에 대한 접근을 제한하고자 하기 때문에 Yes에 체크한다.
Origin Access Identity: CF URL을 통해 S3 버킷에 접근할 사용자를 설정한다. 기존에 생성한 유저가 없담녀 Create a New Identity를 체크한다.
Grant Read Permission on Bucket: S3에 CloudFront가 객체를 읽는 것을 허용하는 정책을 추가해야한다. Yes를 통해 자동으로 S3 버킷 정책에 해당 내용을 추가할 수 있다.

Default Cache Behavior Settings

Signed Url을 사용하기 위해 해당 옵션을 체크해준다.

Distribution Setting

CloudFront Signed Url Java Example

maven dependency

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-aws</artifactId>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk16</artifactId>
    <version>1.46</version>
</dependency>
<dependency>
    <groupId>net.java.dev.jets3t</groupId>
    <artifactId>jets3t</artifactId>
    <version>0.9.4</version>
</dependency>

java Code

package com.cloudfront.guide.s3;


import com.amazonaws.services.cloudfront.CloudFrontCookieSigner;
import com.amazonaws.services.cloudfront.util.SignerUtils;
import lombok.extern.slf4j.Slf4j;
import org.jets3t.service.CloudFrontService;
import org.jets3t.service.CloudFrontServiceException;
import org.jets3t.service.utils.ServiceUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.text.ParseException;

@Slf4j
public class CloudFrontManager {

    /* 사전작업
     * SecretAccess pem key를 아래 명령어로 DER 파일로 변환시킨 후 privateKeyFilePath 경로에 추가한다.
     * openssl pkcs8 -topk8 -nocrypt -in origin.pem -inform PEM -out new.der -outform DER
     */

    private final String distributionDomain = "distribution_Domain"; 
    private final String privateKeyFilePath = "der 파일 위치";
    private final String s3ObjectKey = "1.png";
    private final String policyResourcePath = "http://" + distributionDomain + "/" + s3ObjectKey;
    private final String keyPairId = ""; // CF KeyPair Id

    private byte[] derPrivateKey;

    public CloudFrontManager() throws IOException {
        derPrivateKey = ServiceUtils.readInputStreamToBytes(new FileInputStream(privateKeyFilePath));
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    }

    // 미리 준비된 정책
    public String createSignedUrlCanned() throws ParseException, CloudFrontServiceException {

        String signedUrlCanned = CloudFrontService.signUrlCanned(
                policyResourcePath, // Resource URL or Path
                keyPairId,     // Certificate identifier,
                derPrivateKey, // DER Private key data
                ServiceUtils.parseIso8601Date("2020-11-14T22:20:00.000Z") // DateLessThan
        );
        log.info("Signed Url Canned ====================== {} =========================", signedUrlCanned);

        return signedUrlCanned;
    }

    public String createCustomSingedUrl() throws ParseException, CloudFrontServiceException {

        String policy = CloudFrontService.buildPolicyForSignedUrl(
                // Resource path (optional, can include '*' and '?' wildcards)
                policyResourcePath,
                // DateLessThan
                // 접근 만료시간 세팅
                ServiceUtils.parseIso8601Date("2011-11-14T22:20:00.000Z"),
                // CIDR IP address restriction (optional, 0.0.0.0/0 means everyone)
                "0.0.0.0/0",
                // DateGreaterThan (optional)
                ServiceUtils.parseIso8601Date("2011-10-16T06:31:56.000Z")
        );

        // Generate a signed URL using a custom policy document.

        String signedUrl = CloudFrontService.signUrl(
                // Resource URL or Path
                "http://" + distributionDomain + "/" + s3ObjectKey,
                // Certificate identifier, an active trusted signer for the distribution
                keyPairId,
                // DER Private key data
                derPrivateKey,
                // Access control policy
                policy
        );

        log.info("Signed Url By Custom Policy ====================== {} =========================", signedUrl);

        return signedUrl;

    }


    public void getCloudFrontCookieForCannedPolicy() throws ParseException, InvalidKeySpecException, IOException {

        CloudFrontCookieSigner.CookiesForCannedPolicy cookies = CloudFrontCookieSigner.getCookiesForCannedPolicy(
                SignerUtils.Protocol.http, distributionDomain, new File(privateKeyFilePath), s3ObjectKey,
                keyPairId,  ServiceUtils.parseIso8601Date("2011-11-14T22:20:00.000Z"));
        // 아래 세개의 값을 세팅한다
        /*
        httpGet.addHeader("Cookie", cookiesForCannedPolicy.getExpires().getKey() + "=" +
                cookies.getExpires().getValue());
        httpGet.addHeader("Cookie", cookiesForCannedPolicy.getSignature().getKey() + "=" +
                cookies.getSignature().getValue());
        httpGet.addHeader("Cookie", cookiesForCannedPolicy.getKeyPairId().getKey() + "=" +
                cookies.getKeyPairId().getValue());
        */
    }

}

Attribute

  • distributionDomain: Domain Name에 해당한다.
  • privateKeyFilePath: der 파일의 위치
  • s3ObjectKey: 접근하고자 하는 S3의 객체 key
  • KeyPairId: 보안자격증명의 CloudFront Keypair의 Access Key Id

CloudFrontManagerTest.java

package com.cloudfront.guide.s3;


import org.jets3t.service.CloudFrontServiceException;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;


import java.io.IOException;
import java.text.ParseException;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;


public class CloudFrontManagerTest {

    CloudFrontManager cloudFrontManager;

    @Before
    public void setUp() throws IOException {
        cloudFrontManager = new CloudFrontManager();
    }

    @Test
    public void createSignedUrlCanned() throws ParseException, CloudFrontServiceException, IOException {
        String signedUrl = cloudFrontManager.createSignedUrlCanned();
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity responseEntity = restTemplate.exchange(signedUrl, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()), String.class);
        assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
    }
}

output

Signed URL

http://demyaocudw45p.cloudfront.net/ec2inplacedeploystrategy.md?Expires=1605392400&Signature=t2devX6pf5UtOzsIO~bISRwDEFMgGwDRcazScLFmIrSTihAEPOBqwJ23Ah67Z2WT~pzQIYDSZ5k-qH-Pzcfa2SnknAydo8OR4CYpp9tCDs-S3ZfOZ1PD9SHNGwOAwRpjIajBA-8tjXaN3bOPNMLs3XkLf1cdNrMW7Mzwj-c5a2Y9TuieruRe7A5oND27O~frRRcw-5ypxlNjmUxudIzKAdkjHlmQHl8U7olwNYzDPGCxz9V~R0vsXZ28N-Adlda27rUNSkF6doPJSx25wXtbMU4HULGgQqw8WdFjzN1C2LAWiBdVjJmPDYaVPTjcRiHIOx~YB~i5iWS4KRC5d8MYGg__&Key-Pair-Id=APKAJVEXLCLDXFQVQSPQ

CloudFront Signed Cookie

자세한 예제

<https://medium.com/@himanshuarora/protect-private-content-using-cloudfront-signed-cookies-fd9674faec3