Linode Object Storage でカスタムキーを使ってサーバサイド暗号化する

サーバーサイドの暗号化によって Linode オブジェクトストレージ上のデータを保護できます。本記事は、独自の暗号化キーを使用して、データをディスクに保存する前にオブジェクトレベルで暗号化する方法について記載します。一度暗号化されると、同じ暗号化キーが検索リクエストで提供された場合のみデータを復号化します。現在、Linode オブジェクトストレージが対応している方式は、クライアント側でキーを管理し、サーバーサイドでそのキーを用いて暗号化、復号化する方式です。特に自社で暗号化キーを完全に管理したい場合や、Linode にキーを預けたくない場合に使用される暗号化方式です。


本記事は以下のドキュメントを参考にしています。

https://www.linode.com/docs/guides/server-side-encryption/

上記ドキュメントは、「Hello World!」というテキストを含む単純なファイルを、提供された暗号化キー (SSE-C) を使用して Linode オブジェクトストレージに暗号化してアップロードし、次に復号化してファイルの内容を読み込み、最後にファイルを削除するPythonスクリプトの例が記述されています。

本記事では、用意されたファイルに対して、  Python と AWS Boto3 SDK を用いて、　put、get、delete を実施します。利用するコードは下記から参照してください。

https://github.com/110hideki/sse-c-linode

# SSE-C とは
SSE-C (Server-Side Encryption with Customer-Provided Keys) は、Linode オブジェクトストレージでデータを保存する際にユーザーが自分で管理する暗号化キーを使用して、サーバー側でデータを暗号化する方法です。SSE-C を利用すると、Linode オブジェクトストレージは暗号化プロセスを処理しますが、暗号化キー自体は Linode には保存されず、ユーザーが完全に制御します。大きく３つのポイントがあります。ユーザー提供のキーであること、そのキーはサーバー側に保存されないこと、キーの制御はユーザーに委ねられることです。

## ユーザー提供のキー
ユーザーが暗号化キーを用意し、オブジェクトをアップロードする際に Linode に提供します。Linode オブジェクトストレージはそのキーを使ってデータを暗号化・復号化しますが、キー自体は保存しません。
## キーは保存されない
Linode オブジェクトストレージは暗号化・復号化のリクエストが来た際にのみ、ユーザーが提供したキーを使用します。キーは一時的に使用されるだけです。
## フルコントロール
ユーザーが暗号化キーを完全に管理するため、キーのローテーション（交換）や無効化が自由に行えます。これにより、セキュリティポリシーや法規制に沿った柔軟な運用が可能です。

:::note warn
暗号化キーを変更する場合は、再度暗号化をする必要があります。キーの管理は利用者側の責任となります。キーを紛失した場合は、そのファイルは復号できません。
:::

# 共通設定
API のアクセスキーとシークレットキーが必要になります。作成は下記資料などを参考にしてください。

https://qiita.com/tkmt91/items/93fc1604645c46415503#linode-object-storage-access-key%E3%81%AE%E4%BD%9C%E6%88%90

作成したキーの情報は ``conf.json`` に記載します。
## conf.json
![conf.json.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/0dd35b2c-9b65-25b8-37bf-4b936cd983a8.jpeg)

``endpoint_url`` には アクセスするドメイン名が含まれるエンドポイント URL を記載します。
![bucketname.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/bf55d9cc-e446-9d11-1f5e-8ffef18e02e2.jpeg)


## var.json
![var.conf.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/35e39727-5065-d88d-8cdf-f526c658df16.jpeg)

``encryption_key`` には後述する ``openssl`` コマンドで作成したキーをいれます。``algorithm`` には ``AES256`` が固定値となります。``bucket_name`` にはバケットの中のトップに作成されるフォルダ名を入れます。``s3_path`` はその下のディレクトリ階層をパス形式で記載します。この例では ``myFolder/path1/`` のフォルダの下でオブジェクトを操作します。

# 事前準備
クライアント側で Python3 が動くようになっているか確認します。
```
python3 --version
```
出力結果の例です。
```
Python 3.9.6
```
スクリプトで使用する32文字の暗号化キーを作成します。OpenSSLを使用すると、暗号化キーとして使用する32文字の16進文字を次のコマンドでランダムに生成できます。
```
openssl rand -hex 16
```

:::note warn
Linode オブジェクトストレージはデータを暗号化した後、暗号化キーを直ちに破棄します。SSE-C で暗号化されたオブジェクトストレージデータは、暗号化キーがないと復元できません。
:::

# put_object
put_object はオブジェクトストレージにファイルを保存する関数です。
![S3-Boto3-put.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/340990d3-cae1-fd17-a0c9-8c04cbdf38dc.jpeg)

github にあるコード ``put.py`` を実行する前に、重要な部分のみを説明します。AWS が提供する Boto3 SDK を使いますので事前にインストールしてください。``s3`` を指定することで AWS S3 のインタフェースを使えます。Linode オブジェクトストレージは S3 互換のため、Boto3 SDK を使うときは ``s3`` を指定します。``aws_cfg`` では、前に説明した ``config.json`` の内容を渡しています。
```
　　# S3クライアントの作成
　　client = boto3.client("s3", **aws_cfg)
```

ファイルをオブジェクトストレージに置くには Boto3 SDK の [put_object](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/put_object.html) を使います。ここでは、キー、アルゴリズム、バケット名、キーと値(ファイルの中身) を指定しています。バケット名は実際にはバケット内のトップのフォルダー名になりますので、注意ください。

    client.put_object(
        SSECustomerKey=ENCRYPTION_KEY,
        SSECustomerAlgorithm=ALGO,
        Bucket=BUCKET,
        Key=s3_file_path_name,
        Body=file_data
    )


暗号化したいファイルを用意します。
```
  ls -g input.txt
  -rw-r--r--@ 1 staff  1168 Oct 27 17:14 input.txt
```

Python のコードを実行します。
```
./put.py input.txt
```

次のように表示されたら成功です。
```
Uploading file to Object Storage and encrypting with SSE-C.
Upload and encryption successful.
```
Linode Cloud Manager では次のように見えると思います。
![created-encryptedfile.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/523e626a-3d39-3d76-7ca5-ffb895974755.jpeg)

# get_object
get_object はオブジェクトストレージに存在するファイルを読み取る関数です。
![S3-Boto3-get.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/83ca670a-8e78-957c-ae7d-723e4d7a64af.jpeg)

Boto3 SDK の [get_object](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/get_object.html) を使っています。ここでは、キー、アルゴリズム、バケット名、キーを指定しています。バケット名は実際にはバケット内のトップのフォルダー名になりますので、注意ください。

    client.get_object(
        SSECustomerKey=ENCRYPTION_KEY,
        SSECustomerAlgorithm=ALGO,
        Bucket=BUCKET,
        Key=FILE
    )

第一引数に取得したいファイル名、第二引数にはローカルに落としたいファイル名を渡します。
```
./get.py input.txt output.txt
```
次のような表示がされたら成功しています。
```
Downloading encrypted Object Storage file.
Decrypted object saved to: output.txt
```
ファイルが作成され、``put_object`` で渡したファイルと中身が一緒になっていると思います。
```
ls -g output.txt
-rw-r--r--@ 1 staff  1168 Oct 28 18:18 output.txt
```
## encrytpion_key を指定しない場合

次のコードのように get_objects の引数に encyption_key を指定しない場合はエラーとなりファイルを読み込むことができません。
```
    client.get_object(
        Bucket=BUCKET,
        Key=s3_file_path_name,
    )
```

```
An error occurred: An error occurred (InvalidArgument) when calling the 
GetObject operation: Requests specifying Server Side Encryption with 
Customer provided keys must provide a valid encryption algorithm.
```
このようにファイルを保護できることが分かります。

## 誤った encryption_key を指定した場合
``var.json`` で指定する ``encryption_key`` を誤ると次のエラーとなります。暗号化キーをなくすとファイルを復号化できないことが分かります。
```
An error occurred: An error occurred (InvalidArgument) when calling the 
GetObject operation: The calculated MD5 hash of the key did not match the 
hash that was provided.
```


## 誤った algorithm を指定した場合
``var.json`` で指定するアルゴリズムを変更した結果です。``AES256`` でなければならないことが分かります。

```
An error occurred: An error occurred (InvalidEncryptionAlgorithmError) 　
when calling the GetObject operation: The requested encryption algorithm is 
not valid, must be AES256
```


## 誤った bucket_name を指定した場合
``var.json`` で指定する ``bucket_name`` を誤ると ``NoSuchKey`` と表示されます。Linode では キーとして扱われていることが分かります。
```
An error occurred: An error occurred (NoSuchKey) when calling the GetObject 
operation: Unknown
```

## 誤ったキー（ファイル名）を指定した場合
``get.py``の Python スクリプトで渡すファイル名を誤ると ``NoSuchKey`` と表示されます。
```
An error occurred: An error occurred (NoSuchKey) when calling the GetObject 
operation: Unknown
```

# delete_object
delete_object はオブジェクトストレージに存在するファイルを削除する関数です。
![s3-Boto3-delete.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264188/9ebfad25-aac8-333c-cdd9-c2a22cd1e725.jpeg)

delete_object では暗号化キーを渡す必要はありません。

    client.delete_object(
        Bucket=BUCKET,
        Key=DELETE_FILE
    )

``var.json`` で定義した設定に基づいたパスに対して次のようにファイルを削除できます。
```
./delete.py input.txt
```

本当に削除してよいかプロンプトがでます。 yes を選択するとファイルは削除されます。
```
Are you sure you want to delete 'input.txt' from bucket 'myFolder'? 
Type 'yes' to confirm: yes
Deleting encrypted Object Storage file.
Deletion successful.
```

# まとめ
オブジェクトストレージに配置するファイルを暗号化することは、データ保護とセキュリティの強化、法規制やコンプライアンスの遵守、データ漏洩リスクの低減などのメリットがあります。暗号化により、万が一、バケットへの不正アクセスやセキュリティ侵害が発生しても、暗号化されたデータは復号化されない限り意味を持ちません。データの内容が読み取られないため、重要な情報（個人情報、財務データ、知的財産など）の機密性が保たれます。特に昨今では、PCI DSS、HIPAA、GDPRなど、データの暗号化を要求する法規制が存在します。暗号化機能を使用することで、これらの規制に準拠し、必要なセキュリティ基準を満たすことができます。ファイルが誤って公開されてしまった場合でも、暗号化されていれば、攻撃者や第三者が内容を理解することは困難です。

本記事で紹介したカスタムキーを使ってオブジェクトストレージ側で暗号化する方式はキーの管理が課題にはなりますが、有効なケースが多数ありますので、是非ご検討ください。