AWS Transfer Family 経由で ls コマンドを実行時、Permission deniedエラーの解決方法

2024.02.29

はじめに

AWS Transfer Family for SFTPサーバー経由でls コマンドを実行すると、「Permission denied」エラーが発生した場合の解決方法について説明します。

実際のエラー内容は下記の通りです。

$ sftp -i key名 ユーザー名@transfer familyエンドポイント

sftp> pwd
Remote working directory: /s3バケット名

sftp> ls
Couldn't read directory: Permission denied

sftp> ls test1
test1-1.txt
test1-2.txt

S3バケット配下でlsコマンド実行時、Permission deniedとなり、test1に対して実行するとファイル一覧が確認できます。

S3バケット配下は、AWSマネジメントコンソール上では下記の階層のように見えます。S3は、階層的フォルダ構造ではないため、実際は異なります。

.
├── test.png
├── test1
│   ├── test1-1.txt
│   └── test1-2.txt
└── test2
    └── test2-1.txt

Transfer Familyのユーザー用のIAMロールのポリシーは、下記の通りです

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::s3バケット名"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject<em>",
                "s3:GetObject</em>",
                "s3:DeleteObject*"
            ],
            "Resource": [
                "arn:aws:s3:::s3バケット名/test1/"
            ]
        }
    ]
}

ファイル一覧が確認できるようにS3バケット配下に対して、ListBucketを許可し、test1配下に対して、PutObject、GetObjectなどを許可しています。

Transfer Familyの仕様は下記の通りです

  • プロトコル:SFTP
  • ファイル転送先:S3バケット
  • エンドポイントタイプ:パブリック
  • ユーザー管理方法:サービスマネージド
    • ユーザーのホームディレクトリは、S3バケット

結論

lsコマンド実行時に「Permission denied」エラーが発生するのは、以下の2つの条件に該当する場合です。 いずれかの条件に当てはまらなければ、エラーは解決します。

  1. フォルダオブジェクト (キー名の末尾が / である 0 バイトのオブジェクト) が存在する、かつ、フォルダオブジェクトに対するGetObjectを許可していない
  2. Transfer Familyの最適化されたディレクトリ設定が無効化

1点目に関しては、AWSドキュメントに記載がありました。

Transfer Family がファイルの書き込みまたは一覧表示のHeadObject呼び出しを行う必要がある場合、この呼び出しにはGetObject権限が必要なため、「アクセスが拒否されました」というエラーで失敗します。 この場合、/で終わるすべてのオブジェクトにアクセス許可を追加するポリシー条件を追加することで、アクセスを GetObject 許可できます。これにより、GetObject ファイルが読み取られないようにすることができますが、ユーザーはフォルダーを一覧表示したり、フォルダーを移動したりすることができます 引用

ls コマンドを実行し、一部の結果が Amazon S3 ゼロバイト オブジェクトである場合 (これらのオブジェクトにはスラッシュ文字で終わるキーがあります)、Transfer Familyはこれらの各オブジェクトに対する HeadObject リクエストを発行します引用

今回のエラーに当てはめると、lsコマンド実行時にHeadObjectが行われますが、IAMロールにはフォルダオブジェクトに対するGetObject権限が設定されていないため、「Permission denied」となりました。後述しますが、フォルダオブジェクトが存在しない場合は、「Permission denied」エラーは発生しません。

2点目は、Transfer Familyの最適化されたディレクトリ設定が有効化されている場合は、HeadObjectが実行されないため、GetObject の許可が不要になり、ls コマンドが実行できます。執筆時点では、ドキュメントには記載がなかったため、実際に試した上での結論です。試した内容は後述します。

S3のオブジェクトとフォルダ

S3バケットでは、データは、オブジェクトとして保存され、それぞれが一意の識別子であるキーによって参照されます。

ファイルシステムと異なり、S3はフラットな構造を持ち、階層的フォルダ構造ではありません。

たとえば、images/holiday/photo.jpg というキーがあった場合、images/ というフォルダおよび images/holiday/ というサブフォルダに配置されているように見えますが、これらはS3のフラットなストレージ構造上のため実際には存在しません。この場合のスラッシュ(/)は単なる文字です。

しかし、オブジェクトをグループ化するためのフォルダの概念はサポートされています。

このフォルダは、キーに含まれるスラッシュ(/)によって形成される階層的な名前空間です。

S3のコンソールなどでフォルダを作成すると、「指定したフォルダ名 + 末尾がスラッシュ(/)」である0バイトのオブジェクト(以降、フォルダオブジェクト)が作成されます。

例えば、バケット内にphotosという名前のフォルダを作成すると、photos/というキーで0バイトのオブジェクトが作成され、管理コンソール上ではphotosがフォルダとして表示されます。この場合のスラッシュ(/)はオブジェクト名を意味します。

例として、フォルダオブジェクトが作成される場合は、以下のケースです。

  • コンソール上でフォルダを作成する。
  • S3にアップロードするときに、フォルダごとアップロードする
aws s3 cp ~/directory  s3://bucket-name/ --recursive

以下のように、S3にアップロードするとき、プレフィックス (接頭辞) を指定してアップロードする場合は、フォルダオブジェクトは作成されません。

$ aws s3 cp test.txt s3://bucket-name/prefix

HeadObjectについて

HeadObjectは、オブジェクトからメタデータを取得するAPIです。

HeadObjectを実行した際、リクエストしたオブジェクトの有無と権限によって返すステータスコードが変わります。

  • リクエストしたオブジェクトが存在する
    • s3:GetObject の権限がある場合、200 OKを返す
    • s3:GetObject の権限がない場合、403 Forbiddenを返す
  • リクエストしたオブジェクトが存在しない
    • s3:ListBucket の権限がある場合、404 Not Foundを返す
    • s3:ListBucket の権限がない場合、403 Forbiddenを返す

つまり、ls コマンドを実行すると、ListObjectsが行われたのち、末尾がスラッシュ(/)のオブジェクトに対しては、HeadObjectが行われ、上記の上限分岐から返されるステータスコードが変わります。

フォルダオブジェクトが存在する場合と存在しない場合で、ListObjects と HeadObject の挙動を解説します。

フォルダオブジェクトが存在する場合

フォルダオブジェクトはコンソール上ではなく、以下のコマンドを使用して確認できます。

$ aws s3 ls --recursive s3://S3バケット名
時刻 バイト数 オブジェクト
省略  100  test.png
省略    0  test1/
省略   10  test1/test1-1.txt
省略   50  test1/test1-2.txt
省略    0  test2/
省略   50  test2/test2-1.txt

test1/test2/は、0バイトのため、フォルダオブジェクトです。

バケット直下に対して、Transfer Family 経由でls コマンドを実行しますと、Transfer Family によって以下の API がリクエストされます。

sftp> pwd
Remote working directory: /s3バケット名

sftp> ls
Couldn't read directory: Permission denied
  1. ListObjects
    • s3:ListBucketが許可されているため、バケット内のオブジェクト一覧が返されます。
  2. HeadObject (Key: test1/)
    • フォルダオブジェクト test1/ は存在し、s3:GetObject が許可されているため、200 OK が返されます
  3. HeadObject (Key: test2/)
    • フォルダオブジェクト test2/ は存在するものの、s3:GetObject が許可されていないため、403 Forbidden が返され、Permission deniedが表示されます。

Transfer Family for SFTP サーバーに対して各種SFTPコマンドを実行した際、S3バケットに送信されるリクエストが決定される方法について、ドキュメントに網羅的な情報は掲載されておらず、執筆時点での動作仕様としてご認識下さい。

フォルダオブジェクトが存在しない場合

以下の場合、フォルダオブジェクトが存在しておりません。

$ aws s3 ls --recursive s3://S3バケット名
時刻 バイト数 オブジェクト
省略    100     test.png
省略     10     test1/test1-1.txt
省略     50     test1/test1-2.txt
省略     50     test2/test2-1.txt

Transfer Family サーバー経由で ls コマンドを実行すると、Transfer Familyは、「/」を含むオブジェクト名を仮想的にフォルダとして解釈・表示します。

sftp> pwd
Remote working directory: /s3バケット名

sftp> ls
test.png
test1
test2

フォルダが存在するように見える理由は、バケットに対する ListObjects 実行の結果、バケット内にtest1/test2/ をプレフィックスに持つキー名のオブジェクトが含まれているためです。

  • test1/test1-1.txt
  • test1/test1-2.txt
  • test2/test2-1.txt

これらのオブジェクトが存在しているため、Transfer Family によって S3 バケット内での階層構造の分析が行われ、バケット直下に対して ls コマンドを実行した場合には test1/test2/ といったフォルダが存在するように表示をされます。

バケット直下に対して ls コマンドを実行すると、Transfer Family によって下記の API がリクエストされます。

  1. ListObjects
    • s3:ListBucketが許可されているため、バケット内のオブジェクト一覧(test1/test1-2.txttest1/test1-2.txttest2/test2-1.txt)が返されます。
  2. HeadObject (Key: test1/)
    • test1/は、フォルダオブジェクトでないため、404 Not Found が返されます。「1. ListObjects」の実行結果内容を元に仮想フォルダとしてtest1が表示されます
    • test2/は、フォルダオブジェクトでないため、404 Not Found が返されます。「1. ListObjects」の実行結果内容を元に仮想フォルダとしてtest2が表示されます

HeadObjectの確認

ls コマンド実行時にHeadObjectが実行されているか、Permission deniedエラー時のS3アクセスログをもとに確認します。分析方法は、下記の記事を参考にしました。

同時刻に下記のログが確認できました。

operation key request_uri httpstatus errorcode objectsize
REST.GET.BUCKET - "GET /?list-type=2&delimiter=%2F&max-keys=1000&prefix=&encoding-type=url&fetch-owner=false HTTP/1.1" 200 -
REST.HEAD.OBJECT test2/ "HEAD /test2/ HTTP/1.1" 403 AccessDenied
REST.HEAD.OBJECT test1/ "HEAD /test1/ HTTP/1.1" 200 - 0

ListObjects と HeadObject(HEAD)が実行されていることがわかります。

そのため、ls コマンド実行時には、s3:ListBuckets3:GetObjectを許可する必要があります。

ちなみに、フォルダオブジェクトではない場合、下記の通り404のステータスコードになります。

operation key request_uri httpstatus errorcode objectsize
REST.HEAD.OBJECT test2/ "HEAD /test2/ HTTP/1.1" 404 NoSuchKey

試してみた

再度述べますが、以下の2つの条件に該当する場合にlsコマンドを実行すると「Permission denied」エラーが発生しますので、いずれか条件に当てはまらない場合、エラーが回避されるかを確認してみます。

  1. フォルダオブジェクト (キー名の末尾が / である 0 バイトのオブジェクト) が存在する、かつ、フォルダオブジェクトに対するGetObjectを許可していない
    • 1.GetObjectを許可してみる
  2. Transfer Familyの最適化されたディレクトリ設定が無効化
    • 2.最適化されたディレクトリを有効化してみる

1.GetObjectを許可

ユーザー用のIAMロールは、Transfer Familyのユーザーで設定しています。そのため、ユーザー用のIAMロールにGetObjectを追加します。

再掲ですが、S3バケット配下は、AWSマネジメントコンソール上では下記の階層のように見えます。S3は、階層的フォルダ構造ではないため、実際は異なります。

.
├── test.png
├── test1
│   ├── test1-1.txt
│   └── test1-2.txt
└── test2
    └── test2-1.txt

test1配下に対して、すでにGetObjectが許可されています。test2配下に対してGetObjectを許可します。

下記の通り追加しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::s3バケット名"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject<em>",
                "s3:GetObject</em>",
                "s3:DeleteObject<em>"
            ],
            "Resource": [
                "arn:aws:s3:::s3バケット名/test1/</em>"
            ]
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "s3:GetObject*",
            "Resource": "arn:aws:s3:::s3バケット名/test2/"
        }
    ]
}

実際にSFTP接続して試してみます。

$ sftp> ls
test.png
test1
test2

lsを実行するとファイル一覧が確認できました。

2.最適化されたディレクトリを有効化

Transfer Familyの詳細設定を確認すると、、「最適化されたディレクトリ」が無効になっていることが分かります。

有効化します。

有効化後、サーバーの再起動は不要です。

$ sftp> ls
test.png
test1
test2

ls コマンドを実行するとファイル一覧が確認できました。

S3のアクセスログからAthenaで分析したところ、下記の通り、HeadObjectは実行されないことが確認できました。

operation key request_uri httpstatus errorcode objectsize
REST.GET.BUCKET - "GET /?list-type=2&delimiter=%2F&max-keys=1000&prefix=&encoding-type=url&fetch-owner=false HTTP/1.1" 200 -

S3 バケット直下に対する ls コマンドを実行した場合、HeadObject は行われず、ListObjects のみが実行されるため、s3:GetObject の権限を付与していない場合にも成功するが分かりました。

参考