一覧SC-11-3 · 鍵・秘密情報の寿命

シークレットの配布と復旧

秘密を読める主体と残存コピーを、生成から復旧まで追えるようになります。

説明

秘密を一か所で管理していても、アプリのメモリー、環境変数、ログ、バックアップへコピーされることがあります。元の保存先だけを変更しても、既に配ったコピーは残ります。

公開試料の配布台帳で読取り権限とコピー先を調べます。実際の子プロセスへの環境変数継承を確認した後、世代更新と旧値の失効、バックアップからの復旧を台帳へ反映します。

秘密を読める主体と残存コピーを、生成から復旧まで追えるようになります。

1保管先と権限を対応付ける

資料にはvault、app-cache、backup、debug-logの4コピーがあります。ローテーション権限は読取り権限と別に記録します。

from sc11 import copies import os,sys,subprocess db = copies() print(db.execute("SELECT * FROM copies保存されたコピーを一覧にする ORDER BY place").fetchall()) print(db.execute("SELECT * FROM grants主体ごとの権限を確認する ORDER BY subject,action").fetchall())

supportが読めるdebug-logにもv1があるため、保管庫の権限だけでは秘密の配布範囲を説明できません。

2読取り権限を持つ主体だけを抽出する

action=readを条件にし、rotateを持つopsと分けます。

print(db.execute("SELECT g.subject,c.place,c.version FROM grants g JOIN copies c USING(place) WHERE g.action='read'読取りだけを抽出する ORDER BY g.subject").fetchall())

app、restore、supportがそれぞれ別の場所から読めます。鍵を更新できる権限と平文を取り出せる権限を一括にしない設計も可能です。

3子プロセスへの環境変数継承を確認する

公開試料値を子プロセスの環境へ渡します。利用者の環境変数は読み取りません。

child_env = {"LBOX_PUBLIC_DEMO":"example-only"} child_code = "import os; print(os.environ.get('LBOX_PUBLIC_DEMO','missing'))" captured = subprocess.check_output([sys.executable,"-c",child_code],env=child_env指定した公開試料だけを子へ渡す,text=True).strip() print(captured)子が読み取った値を確認する

子プロセスにexample-onlyが渡りました。環境変数もプロセスへ配るコピーの一つです。

4配布元から除いても取得済み値は残る

次に作る子には環境変数を渡さず、前に子が返した値と比較します。

child_env.clear()今後の配布設定から値を除く print(subprocess.check_output([sys.executable,"-c",child_code],env=child_env,text=True).strip()) print(captured)取得済みの値が残ることを確認する

新しく起動した子プロセスではmissingになりますが、既存のプロセスが取得した値は残ります。設定を変更しても、過去に渡した値は回収されません。

5ログのコピーを除去して残りを調べる

台帳上でdebug-logのコピーを削除します。バックアップとアプリ内のコピーはそのままです。

db.execute("DELETE FROM copiesログ側のコピーを除く WHERE place='debug-log'") print(db.execute("SELECT place FROM copies WHERE version='v1'旧値の残存先を調べる ORDER BY place").fetchall())

app-cache、backup、vaultが残ります。一か所の除去だけで、値が存在しなくなったとは判断できません。

6新しい世代へ更新し旧値を失効させる

vaultをv2へ変更し、認証側のactive表でv1を無効にします。値の保有と利用可否を分けた例です。

db.execute("INSERT INTO active VALUES('v2',1)") db.execute("UPDATE active SET enabled=0旧値の利用を認証側で止める WHERE version='v1'") db.execute("UPDATE copies SET version=保管庫を新世代へ更新する'v2' WHERE place='vault'") print(db.execute("SELECT c.place,c.version,a.enabled FROM copies c JOIN active a USING(version) ORDER BY c.place").fetchall())

旧コピーは残っても、認証側の状態では利用不可です。暗号鍵の場合、コピーを持つ相手のオフライン復号までこの表で止められるわけではありません。

7復旧で旧値を戻した場合を確認する

backupのv1をapp-cacheへ戻します。復旧時にも現在の有効世代を照合する必要があります。

db.execute("UPDATE copies SET version=(SELECT version FROM copies WHERE place='backup'復旧元の世代を取得する) WHERE place='app-cache'") print(db.execute("SELECT c.place,c.version,a.enabled現在の有効状態を照合する FROM copies c JOIN active a USING(version) WHERE c.place='app-cache'").fetchone())

app-cacheはv1ですがenabled=0です。復旧した設定が正しく動くか、失効済みの秘密を再導入していないかを確認する必要があります。

まとめ

秘密の所在は、保管庫だけでなく配布済みコピーも含めて調べます。

設定から除く操作と、取得済み値の回収は別です。

バックアップ復旧時も、現在の有効世代と失効状態を照合します。

操作履歴と解説を振り返れます。

Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻

1
from sc11 import copies import os,sys,subprocess db = copies() print(db.execute("SELECT * FROM copies ORDER BY place").fetchall()) print(db.execute("SELECT * FROM grants ORDER BY subject,action").fetchall())
[('app-cache', 'v1', 'app'), ('backup', 'v1', 'restore'), ('debug-log', 'v1', 'support'), ('vault', 'v1', 'app,ops')]
[('app', 'vault', 'read'), ('ops', 'vault', 'rotate'), ('restore', 'backup', 'read'), ('support', 'debug-log', 'read')]
2
print(db.execute("SELECT g.subject,c.place,c.version FROM grants g JOIN copies c USING(place) WHERE g.action='read' ORDER BY g.subject").fetchall())
[('app', 'vault', 'v1'), ('restore', 'backup', 'v1'), ('support', 'debug-log', 'v1')]
3
child_env = {"LBOX_PUBLIC_DEMO":"example-only"} child_code = "import os; print(os.environ.get('LBOX_PUBLIC_DEMO','missing'))" captured = subprocess.check_output([sys.executable,"-c",child_code],env=child_env,text=True).strip() print(captured)
example-only
4
child_env.clear() print(subprocess.check_output([sys.executable,"-c",child_code],env=child_env,text=True).strip()) print(captured)
missing
example-only
5
db.execute("DELETE FROM copies WHERE place='debug-log'") print(db.execute("SELECT place FROM copies WHERE version='v1' ORDER BY place").fetchall())
[('app-cache',), ('backup',), ('vault',)]
6
db.execute("INSERT INTO active VALUES('v2',1)") db.execute("UPDATE active SET enabled=0 WHERE version='v1'") db.execute("UPDATE copies SET version='v2' WHERE place='vault'") print(db.execute("SELECT c.place,c.version,a.enabled FROM copies c JOIN active a USING(version) ORDER BY c.place").fetchall())
[('app-cache', 'v1', 0), ('backup', 'v1', 0), ('vault', 'v2', 1)]
7
db.execute("UPDATE copies SET version=(SELECT version FROM copies WHERE place='backup') WHERE place='app-cache'") print(db.execute("SELECT c.place,c.version,a.enabled FROM copies c JOIN active a USING(version) WHERE c.place='app-cache'").fetchone())
('app-cache', 'v1', 0)

演習完了