OCI Valutシークレットを使ってセキュアにキーを保存

## Vaultシークレット

OCI Vaultは暗号鍵の管理だけでなく、シークレットと呼ばれるパスワードやAPIキーをセキュアに管理・運用する機能もあります。
無償枠が十分大きいのでこの枠内でほぼ収まるかと思います。
またロギングにて、いつ、だれがキー取得操作をしたかを追跡可能です。

https://speakerdeck.com/oracle4engineer/oci-vault-ji-neng-gai-yao-tote-zheng?slide=28

## 使い方

Python SDKを使って下記一連の操作を実行していきます：
* シークレット作成
* 更新
* 一覧取得
* 値取得
* 削除


### 事前準備

Vault自体の作成と、AESのマスター暗号化キーを作成しておきます。
以下vault_idとkey_idはそのOCIDです。


### シークレット作成

```py:
import oci
import json
import base64
import uuid 

vault_id = 'ocid1.vault.oc1.iad.'
compartment_id = 'ocid1.compartment.oc1..'
key_id = "ocid1.key.oc1.iad."

config = oci.config.from_file()
vault_client = oci.vault.VaultsClient(config)

secret_data = {
    "dbPassword": "dbpass_pwd",
    "apiKey": "apikey_pwd",
    "adminToken": "admintoken_pwd"
}
secret_json_str = json.dumps(secret_data)
secret_encoded = base64.b64encode(secret_json_str.encode('utf-8')).decode('utf-8')
secret_content_details = oci.vault.models.Base64SecretContentDetails(
    content_type="BASE64",
    content=secret_encoded
)

secret_name = 'secret_20241211'
create_secret_details = oci.vault.models.CreateSecretDetails(
    compartment_id=compartment_id,
    key_id=key_id,
    secret_name=secret_name,
    vault_id=vault_id,
    secret_content=secret_content_details,
)
create_secret_response = vault_client.create_secret(
    create_secret_details=create_secret_details,
    opc_request_id=str(uuid.uuid4()),
    opc_retry_token=str(uuid.uuid4())
)
secret_ocid = create_secret_response.data.id
print(f"シークレット '{secret_name}' が作成されました。OCID: {secret_ocid}")
```
シークレット名「secret_20241211」でパスワードはjson形式で保存します。
実行するとOCIDが払い出されます。このシークレットのOCIDは後からでも取得可能です。

```
シークレット 'secret_20241211' が作成されました。OCID: ocid1.vaultsecret.oc1.iad...
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/3fa9bdd0-ce9e-3d52-61ea-5f59e91b3cda.png)



### 更新

先ほどのシークレットを更新します。secret_idは先ほどのシークレットOCIDです。
```py
config = oci.config.from_file()
vault_client = oci.vault.VaultsClient(config)

secret_data = {
    "dbPassword": "dbpass_pwd_v2",
    "apiKey": "apikey_pwd_v2",
    "adminToken": "admintoken_pwd_v2"
}
secret_json_str = json.dumps(secret_data)
secret_encoded = base64.b64encode(secret_json_str.encode('utf-8')).decode('utf-8')
secret_content_details = oci.vault.models.Base64SecretContentDetails(
    content_type="BASE64",
    content=secret_encoded
)

update_details = oci.vault.models.UpdateSecretDetails(
    secret_content=secret_content_details
)
response = vault_client.update_secret(
    secret_id=secret_id,
    update_secret_details=update_details
)
```

コンソール上で見るとバージョンが更新されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/eb6a0312-c54b-7072-06e6-44d71ff1b125.png)


### 一覧
シークレットを一覧を取得します。
現在は一つしかありませんが複数あればステータス含めて取得できます。

```py
config = oci.config.from_file()
vault_client = oci.vault.VaultsClient(config)

all_secrets = []
list_secrets_response = vault_client.list_secrets(
    compartment_id=compartment_id,
    vault_id=vault_id,
    limit=100 
)
all_secrets.extend(list_secrets_response.data)

for secret in all_secrets:
    if secret.lifecycle_state != 'ACTIVE': 
        continue
    print(f"シークレット名: {secret.secret_name}, OCID: {secret.id}, Status: {secret.lifecycle_state}")
    secret_versions_response = vault_client.list_secret_versions(
        secret_id=secret.id,
        limit=100
    )
    secret_versions = secret_versions_response.data
    for version in secret_versions:
        print(version)
```

下記のように取得できます。
キーがローテーションされ、最新はversion_number=2となります。
```json:結果
{
  "content_type": null,
  "is_content_auto_generated": false,
  "name": null,
  "secret_id": "ocid1.vaultsecret.oc1.iad.",
  "stages": [
    "CURRENT",
    "LATEST"
  ],
  "system_tags": null,
  "time_created": "2024-12-11T02:09:44.068000+00:00",
  "time_of_deletion": null,
  "time_of_expiry": null,
  "version_number": 2
}
{
  "content_type": null,
  "is_content_auto_generated": false,
  "name": null,
  "secret_id": "ocid1.vaultsecret.oc1.iad.",
  "stages": [
    "PREVIOUS"
  ],
  "system_tags": null,
  "time_created": "2024-12-11T01:53:38.783000+00:00",
  "time_of_deletion": null,
  "time_of_expiry": null,
  "version_number": 1
}

```

### 取得
シークレット値を取得します。
先ほどローテーションしたのでそれぞれ取得してみます。
何も指定しないと最新が取得されます。

```py:最新取得
secrets_client = oci.secrets.SecretsClient(config)
secret_bundle = secrets_client.get_secret_bundle(secret_id=secret_id).data
secret_encoded = secret_bundle.secret_bundle_content.content
secret_decoded = base64.b64decode(secret_encoded).decode('utf-8')
secret_json = json.loads(secret_decoded)
print(secret_json)
```
```json:最新
{'dbPassword': 'dbpass_pwd_v2', 'apiKey': 'apikey_pwd_v2', 'adminToken': 'admintoken_pwd_v2'}
```

続いて、更新前の古い結果（version=1）を取得します。

```py:古い結果取得
secrets_client = oci.secrets.SecretsClient(config)
secret_bundle = secrets_client.get_secret_bundle(secret_id=secret_id, version_number=1).data
secret_encoded = secret_bundle.secret_bundle_content.content
secret_decoded = base64.b64decode(secret_encoded).decode('utf-8')
secret_json = json.loads(secret_decoded)
print(secret_json)
```
```json:古い結果
{'dbPassword': 'dbpass_pwd', 'apiKey': 'apikey_pwd', 'adminToken': 'admintoken_pwd'}
```

### 削除
特定シークレットを削除します。
※下はシークレット削除ですが、バージョン単位での削除も可能です。

```py
vault_client = oci.vault.VaultsClient(config)
schedule_deletion_details = oci.vault.models.ScheduleSecretDeletionDetails(
    time_of_deletion=datetime.now() + timedelta(days=7)
)
vault_client.schedule_secret_deletion(
    secret_id=secret_id,
    schedule_secret_deletion_details=schedule_deletion_details
)
print(f"Deletion scheduled for secret: {secret_id}")
```
```txt:実行結果
Deletion scheduled for secret: ocid1.vaultsecret.oc1.iad.
```

現時刻から7日後に削除するよう設定しており、実行後は非アクティブとなります（Status: PENDING_DELETION）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/07c2e344-810a-dcd2-a92a-c379511bea8c.png)


## 参考リンク

https://docs.oracle.com/ja-jp/iaas/Content/KeyManagement/Tasks/managingsecrets.htm

https://docs.oracle.com/en-us/iaas/tools/python/2.139.0/api/vault.html

https://qiita.com/500InternalServerError/items/5f5c546c915a1b8f3fbd