Amazon S3 PrivateLink 経由で AWS CLI も SDK も使わず Curl で Get してみた

オンプレミスから Amazon S3 PrivateLink 経由で curl したいよーという時に。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

コンバンハ、千葉(幸)です。

先日、 Amazon S3 が AWS PrivateLink(インターフェース型 VPC エンドポイント) に対応しました。これにより、オンプレミスからプライベート IP を使用して S3 バケットに対してアクセスできるようになりました。

公式ドキュメントややってみたブログでは AWS CLI や AWS SDK を用いたケースが取り上げられています。そういった方式による認証を使用せず、単純な HTTPS アクセスで S3 上のファイルを取得したいと思い、検証してみました。

先にまとめ

  • バケットポリシーで許可すれば IAM の認証情報なしで取得可能
  • パス形式の場合、エンドポイントの DNS 名のサブドメインは「なし」か特定のものである必要あり
  • 仮想ホスト形式の場合、名前解決の部分を頑張る必要がある

想定している構成

イメージとしては以下の通りです。

Direct Connectをなどを通じてオンプレミスと VPC が接続されており、オンプレミス上のクライアントが S3 にプライベートアクセスをしたいです。

クライアントには AWS CLI などのツールをセットアップせず、単純な Curl で S3 バケット内のオブジェクトを Get したいとします。

検証する構成

再現するのがハードルが高そうだったので、以下のお手軽構成で検証します。同一 VPC のインスタンスから接続します。

ルートテーブルや SecuriryGroup などのネットワーク設定、名前解決の部分を考慮すれば、 VPC の中からでも外からでも変わらないだろうという目論見です。

下準備

S3 インターフェースエンドポイント

設定済みであるとします。クライアントの EC2 インスタンスと疎通が取れる状態です。

S3 バケット

対象の S3 バケットはchibayuki-from-vpceとし、以下のバケットポリシーを設定しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::chibayuki-from-vpce/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceVpce": "vpce-0562f2c59ac7b66e6"
                }
            }
        }
    ]
}

ポリシー内で指定している vpce-id は S3 インターフェースエンドポイントのものです。

その他の設定は以下です。

  • バケットのパブリックアクセスブロックをすべてオン
  • バケット ACL はデフォルト(所有者のみ読み書き可)
  • オブジェクト ACL もデフォルト(所有者のみ読み書き可)

バケット内には以下の慎ましいファイルを格納しています。

Hello.txt

Hello

クライアント

クライアントの EC2 は Amazon Linux2 で、curl のバージョンは以下です。

curl --version
curl 7.61.1 (x86_64-koji-linux-gnu) libcurl/7.61.1 OpenSSL/1.0.2k zlib/1.2.7 libidn2/2.3.0 libssh2/1.4.3 nghttp2/1.41.0
Release-Date: 2018-09-05
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz HTTP2 UnixSockets HTTPS-proxy Metalink

やってみた

早速いくつかのパターンでアクセスしていきます。

前提として S3 PrivateLink はプライベートDNS 名に対応していないという仕様があるため、エンドポイント固有の DNS 名を指定します。

この辺りの詳細は以下エントリをご参照ください。

ちなみに今回のエンドポイントのリージョナル DNS 名は以下です。

*.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com

手元の端末で名前解決を試みると、プライベート IP アドレスが返却されます。

% nslookup chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com
Server:		218.xx.82.xx
Address:	218.xx.82.xx#53

Non-authoritative answer:
Name:	chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com
Address: 192.168.3.63
Name:	chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com
Address: 192.168.1.66

名前解決の際にはエンドポイントの先頭の*には任意の値を入れられますし、削っても問題ありません。

クライアントがオンプレミスにいることを想定した場合、(少なくともパス形式では)この DNS 名を名前解決できる必要があります。

パス形式で HTTP

一番シンプルなパターンで試します。

*を削った エンドポイント名の後に/バケット名/オブジェクト名を付与して curl します。

[root@ip-192-168-0-168 ~]# curl http://vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
Hello

問題なく Get できました。

-Oオプションを指定してダウンロードもできます。

[root@ip-192-168-0-168 ~]# curl -O http://chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   364    0   364    0     0  22750      0 --:--:-- --:--:-- --:--:-- 22750
[root@ip-192-168-0-168 ~]# ls | grep Hello
Hello.txt

エンドポイントのサブドメインに注意

いくつか試行する中で気付いたのですが、この形式でアクセスする場合のサブドメインは任意のものが使えるわけではないようです。ひとまず以下で成功することは確認できました。

  • なし(上記のパターン)
  • bucket

bucketを指定した場合は上記と同様に成功します。

[root@ip-192-168-0-168 ~]# curl http://bucket.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
Hello

適当な値、例えばchibasaitamaを指定した場合はエラーになります。

[root@ip-192-168-0-168 ~]# curl http://chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com</BucketName><RequestId>001HN8C12GPACH9J</RequestId><HostId>woMYg8S5RldFCQyMiiO30+4ueRmWrWSZMVo+KOssOt5Ge2cFaQn6o/eSEP+Uz9CZ8aE7eSZxWtc=</HostId></Error>
[root@ip-192-168-0-168 ~]# curl http://saitama.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>saitama.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com</BucketName><RequestId>YNEH987YXCACFRA2</RequestId><HostId>z1anjCV4SafrtMSGN0cH34CdakE7/lX/554uUZxT98vCpIVCyfRzXkolGQ7HlWNTIuDCuJMY+tc=</HostId></Error>

エンドポイント名がバケット名と解釈されるために発生しているエラーのようです。不思議仕様ですね。bucket以外にも使用できるものはありそうですが、情報が見つかりませんでした。

IP アドレス直指定だとどうなる?

http://エンドポイントのIP/バケット名/オブジェクト名で試してみました。

[root@ip-192-168-0-168 ~]# curl http://192.168.1.66/chibayuki-from-vpce/Hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>PermanentRedirect</Code><Message>The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint.</Message><Endpoint>chibayuki-from-vpce.s3.amazonaws.com</Endpoint><Bucket>chibayuki-from-vpce</Bucket><RequestId>Y9ESJ9Z750YKECS5</RequestId><HostId>A9wDITlwdt1AdyoUoHWaaxIGdobMDyW8lpg9QgSpA/8CvkzwCSnNzA5yQI7VsHvWEkB540Ir9Bk=</HostId></Error>

エンドポイント名としてchibayuki-from-vpce.s3.amazonaws.comを指定しろ、というエラーになりました。難しいですね。

パス形式で HTTPS

bucketを指定した場合は特に変わらず成功します。

[root@ip-192-168-0-168 ~]# curl https://bucket.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
Hello

chibaを指定した場合は SSL のエラーが出ます。

[root@ip-192-168-0-168 ~]# curl https://chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
curl: (51) SSL: no alternative certificate subject name matches target host name 'chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com'

-kを指定して無視すれば、HTTP の場合と同じエラーになります。

[root@ip-192-168-0-168 ~]# curl -k https://chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com/chibayuki-from-vpce/Hello.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>chiba.vpce-0562f2c59ac7b66e6-4j54175z.s3.ap-northeast-1.vpce.amazonaws.com</BucketName><RequestId>CQYN8CNJYPFJN1VN</RequestId><HostId>ZI2dgHwR8QhFmHgNY8UBe9mZV7vG1vHk6d/P9oUfBN8Td080brcHB+9TYmgXLkcJW/m1jw1ImiY=</HostId></Error>

仮想ホスト形式で HTTP

仮想ホスト形式とは、東京リージョンの S3 バケットであれば以下のような形式で指定するものです。

  • https://バケット名.s3.ap-northeast-1.amazonaws.com

これをどう実現するかは悩ましいですが、以下のような力業でやってみました。

curl --resolve chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com:80:192.168.1.66\
  http://chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com/Hello.txt
 Hello

コマンド内で指定している192.168.1.66はインターフェースエンドポイントの IP アドレスです。

-vオプションを付与すると以下のような形で流れが確認できます。

[root@ip-192-168-0-168 ~]# curl -v --resolve chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com:80:192.168.1.66  http://chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com/Hello.txt
* Added chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com:80:192.168.1.66 to DNS cache
* Hostname chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com was found in DNS cache
*   Trying 192.168.1.66...
* TCP_NODELAY set
* Connected to chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com (192.168.1.66) port 80 (#0)
> GET /Hello.txt HTTP/1.1
> Host: chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< x-amz-id-2: KMFqlsbu6XP4nAH41trufO8NI74GvobqKRocsaNo3Y+/xrSIHWNk+RjRpaAe7yFcC28kA6ioYo0=
< x-amz-request-id: W41M3HJAR162B1X8
< Date: Wed, 31 Mar 2021 12:06:26 GMT
< Last-Modified: Wed, 31 Mar 2021 10:51:34 GMT
< ETag: "8b1a9953c4611296a827abf8c47804d7"
< Accept-Ranges: bytes
< Content-Type: text/plain
< Content-Length: 5
< Server: AmazonS3
<
* Connection #0 to host chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com left intact
Hello

やっていることは/etc/hostsにエントリを追加するのと同じですね。

同じようなことを試しているパターンは以下をご参照ください。

仮想ホスト形式で HTTPS

--resolveオプションを使用するパターンで HTTPS に替えて試してみます。

[root@ip-192-168-0-168 ~]# curl --resolve chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com:443:192.168.1.66\
>   https://chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com/Hello.txt
curl: (51) SSL: no alternative certificate subject name matches target host name 'chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com'

ここでも証明書エラーが発生しています。

-kと、折角なので-vを付与して試してみました。

[root@ip-192-168-0-168 ~]# curl -k -v --resolve chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com:443:192.168.1.66\
>   https://chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com/Hello.txt
* Added chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com:443:192.168.1.66 to DNS cache
* Hostname chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com was found in DNS cache
*   Trying 192.168.1.66...
* TCP_NODELAY set
* Connected to chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com (192.168.1.66) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-SHA
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: CN=s3.ap-northeast-1.amazonaws.com
*  start date: Mar 21 00:00:00 2021 GMT
*  expire date: Apr 19 23:59:59 2022 GMT
*  issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
*  SSL certificate verify ok.
> GET /Hello.txt HTTP/1.1
> Host: chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< x-amz-id-2: pN9LMggrOk45bf+WyTUtOe+6SDncTDTEEUgmwFxft9tr/BvOWyxg2BAJS+d2vWP6EP2Qo5RzMSU=
< x-amz-request-id: AW59F9D2NH1H6HBY
< Date: Wed, 31 Mar 2021 12:21:30 GMT
< Last-Modified: Wed, 31 Mar 2021 10:51:34 GMT
< ETag: "8b1a9953c4611296a827abf8c47804d7"
< Accept-Ranges: bytes
< Content-Type: text/plain
< Content-Length: 5
< Server: AmazonS3
<
* Connection #0 to host chibayuki-from-vpce.s3-ap-northeast-1.amazonaws.com left intact
Hello

諸々の処理を経て、最終的に Get できました。力業感がすごいですね。

終わりに

いくつかのパターンで Curl を試してみました。

バケットポリシーを適切に設定していれば、 IAM の認証情報がなくてもオブジェクトを取得できることが分かりました。

仮想ホスト形式の場合は、プライベートなホストゾーンでレコードを設定しておけば力業感少なく取得が実現できるかもしれません。

パス形式でのリクエストは一度廃止がアナウンスされましたが、廃止タイミングは延期になっている状態です。

パス形式の方がお手軽ですが、使用の際には将来的な廃止の可能性も念頭において採用ください。

以上、千葉(幸)がお送りしました。