本文へ移動
BlackOps1.xは試験的なバージョンです。Production Readyは2.xを予定しています。Releases
BlackOps
Esc
navigateopen⌘Jpreview
このページの内容

Tenant and Storage Protection

Tenant Provider、BOPD Protected Schema、認可、Key Rotationを導入し安全性を確認する。

このページは、Tenant付きOperationとFramework-owned Protected Storageを安全に導入するための実装済みJourneyです。BlackOps 1.xはExperimentalで、旧Plaintext StorageとのRuntime互換はありません。公開済みExperimental Stable 1.2.0を固定してから手順を実行してください。

1. Tenantを入口で確定する

Tenantは認証CredentialやOperationValueから推測しません。BlackOps\Core\TenantRefはOpaqueなtypeidだけを持つ不変Value Objectです。Tenant IDを知っていることはAuthorizationではありません。

入口ごとのProviderをApplicationで構成します。

入口 TenantのSource 伝播
HTTP AuthenticationResultまたはApplication-owned Resolverが検証したTenantRef AnonymousはTenantなし。未検証Headerだけでは設定しない
Console BlackOps\Console\ConsoleTenantProvider Actor Providerとは別に構成
Scheduled BlackOps\Scheduling\ScheduledTenantProvider Scheduleごとに明示。失敗時はTenantなしへFallbackしない
直接Dispatch Dispatcher::dispatch(..., ?TenantRef $tenant = null) 呼出側が末尾引数で明示

ExecutionContext::tenant()は読み取り専用です。Child、Deferred Worker、Retry、Lease Recovery、Transactional Outboxは親Tenantを不変に継承します。Cross-tenant処理はTenant Overrideではなく、専用Authorizationを持つ別Rootとして設計します。

HTTP AuthenticatorはCredentialを検証した後だけTenantを返します。Tenant Headerをそのまま信頼しません。

use BlackOps\Http\Authentication\AuthenticationResult;
use BlackOps\Core\TenantRef;

return AuthenticationResult::authenticated(
    actor: $verifiedActor,
    tenant: new TenantRef('account', $verifiedAccountId),
);

Quickstartの/invoicesをこのTenant付きHTTP Journeyで動かす場合は、既存のexamples/quickstart/app/UserInterface/Http/SampleTokenAuthenticator.phpauthenticate()成功分岐を、次の完全な置換へ変更します。Tokenの空/不一致分岐は既存のままです。

use BlackOps\Core\ActorRef;
use BlackOps\Core\TenantRef;
use BlackOps\Http\Authentication\AuthenticationResult;

return AuthenticationResult::authenticated(
    actor: new ActorRef('quickstart-user', 'user'),
    tenant: new TenantRef('account', 'local-example'),
);

この変更後にQuickstartのHTTPをTenant付きにするを確認し、build:compileしてから、後述の/invoices curlでHTTP 202とstatus: "accepted"を確認します。Productionではlocal-exampleをToken検証後のApplication-owned Tenant Resolverへ置き換えます。

Console/Scheduled ProviderはActor Providerと別にBindingします。

use BlackOps\Console\ConsoleTenantProvider;
use BlackOps\Core\ScheduleContext;
use BlackOps\Core\TenantRef;
use BlackOps\Scheduling\ScheduledTenantProvider;

final readonly class ApplicationConsoleTenantProvider implements ConsoleTenantProvider
{
    public function __construct(private ApplicationTenantResolver $tenants) {}

    public function tenant(): ?TenantRef
    {
        return $this->tenants->forConsole();
    }
}

final readonly class ApplicationScheduledTenantProvider implements ScheduledTenantProvider
{
    public function __construct(private ApplicationTenantResolver $tenants) {}

    public function tenant(ScheduleContext $context): ?TenantRef
    {
        return $this->tenants->forSchedule($context);
    }
}

ApplicationTenantResolverはApplicationが所有する実装です。Consoleの認証済みOperator Context、またはScheduledのScheduleContextから検証済みTenantRefを返し、未登録・解決失敗を別TenantやTenantなしへFallbackしません。TenantRefをConstructorへ直接置くとCompiled Containerが値を自動生成できないため、Resolverを介してください。Resolverと2つのProviderは、Containerで解決できるApplication ServiceだけをConstructorへ受け取る形にします。

ResolverのApplication-owned Contractは次の形です。実装はApplicationの認証済みConsole Context/Schedule Contextを読み、Contextがなければnullを返してください。実装Class(例:ConfiguredApplicationTenantResolver)をInterfaceへBindingし、ProviderのConstructorへTenantRefを直接渡さないことが重要です。

interface ApplicationTenantResolver
{
    public function forConsole(): ?TenantRef;

    public function forSchedule(ScheduleContext $context): ?TenantRef;
}

final readonly class ConfiguredApplicationTenantResolver implements ApplicationTenantResolver
{
    public function forConsole(): ?TenantRef
    {
        return new TenantRef('account', 'local-example');
    }

    public function forSchedule(ScheduleContext $context): ?TenantRef
    {
        return new TenantRef('account', 'local-example');
    }
}

ConfiguredApplicationTenantResolverは実行可能なLocal例です。Productionではlocal-exampleを使わず、Console Operator Context/Schedule Contextから認証済みTenantを解決するApplication実装へ置き換えてください。Frameworkはその取得方法を決めませんが、Resolverを具象ClassとしてApplicationTenantResolver::classへBindingしてからBuildする点は変わりません。

2. Key ProviderをApplicationへ登録する

Protected Storageを使うRuntimeではStorageKeyProviderが必須です。Quickstartの登録境界はApplication Service Providerです。

use App\Security\ApplicationTenantResolver;
use App\Security\ConfiguredApplicationTenantResolver;
use App\Security\SampleStorageKeyProvider;
use App\Security\ApplicationOperationDataReadAuthorizer;
use App\Security\ApplicationOperationStatusAuthorizer;
use App\Security\ApplicationConsoleTenantProvider;
use App\Security\ApplicationScheduledTenantProvider;
use BlackOps\Console\ConsoleTenantProvider;
use BlackOps\Core\DependencyInjection\ServiceProvider;
use BlackOps\Core\DependencyInjection\ServiceRegistry;
use BlackOps\StorageProtection\StorageKeyProvider;
use BlackOps\OperationData\OperationDataReadAuthorizer;
use BlackOps\Status\OperationStatusAuthorizer;
use BlackOps\Scheduling\ScheduledTenantProvider;

final readonly class ApplicationServiceProvider implements ServiceProvider
{
    public function register(ServiceRegistry $services): void
    {
        $services->autowire(ApplicationTenantResolver::class, ConfiguredApplicationTenantResolver::class);
        $services->autowire(StorageKeyProvider::class, SampleStorageKeyProvider::class);
        $services->autowire(OperationDataReadAuthorizer::class, ApplicationOperationDataReadAuthorizer::class);
        $services->autowire(OperationStatusAuthorizer::class, ApplicationOperationStatusAuthorizer::class);
        $services->autowire(ConsoleTenantProvider::class, ApplicationConsoleTenantProvider::class);
        $services->autowire(ScheduledTenantProvider::class, ApplicationScheduledTenantProvider::class);
    }
}

Raw Value、Outcome、Reason、ResponseをPolicyへ渡さず、Clear SubjectのActor/Tenant一致だけで最小のAllow/Denyを返す例です。ProductionではResource、Purpose、Role、Membership、Operation Typeの条件をApplication Policyへ追加してください。

use BlackOps\OperationData\OperationDataReadAuthorizationDecision;
use BlackOps\OperationData\OperationDataReadAuthorizationRequest;
use BlackOps\OperationData\OperationDataReadAuthorizer;

final readonly class ApplicationOperationDataReadAuthorizer implements OperationDataReadAuthorizer
{
    public function decide(OperationDataReadAuthorizationRequest $request): OperationDataReadAuthorizationDecision
    {
        $currentActor = $request->currentActor();
        $originActor = $request->originActor();
        $currentTenant = $request->currentTenant();
        $originTenant = $request->originTenant();

        if ($currentActor === null || $originActor === null || $currentTenant === null || $originTenant === null) {
            return OperationDataReadAuthorizationDecision::deny();
        }

        return $currentActor->type() === $originActor->type()
            && $currentActor->id() === $originActor->id()
            && $currentTenant->type() === $originTenant->type()
            && $currentTenant->id() === $originTenant->id()
            ? OperationDataReadAuthorizationDecision::allow()
            : OperationDataReadAuthorizationDecision::deny();
    }
}

Status参照は同じPolicyへ混ぜず、既存のPolicyを実装してBindingするにあるOperationStatusAuthorizerの完全例を使います。両方をApplication Service ProviderへBindingし、Authorizerへ復号済みDataを渡さないでください。

ProviderはSecret Manager/KMS等からKeyを解決し、次の実装済みContractだけを満たします。上のProvider名はApplication-owned Classへ置き換え、Key MaterialをExampleへ書きません。

public function activeKey(?TenantRef $tenant, StoragePurpose $purpose): StorageKey;
public function key(string $keyId, ?TenantRef $tenant, StoragePurpose $purpose): StorageKey;

QuickstartのLocalでは、既存のexamples/quickstart/app/Security/SampleStorageKeyProvider.phpをそのままApplication-owned ProviderとしてBindingできます。Environmentからstrict base64の32-byte Secretを読み、Active/歴代Key IDを分けて解決する完全な例です。Secret値そのものは掲載しません。

use BlackOps\Core\TenantRef;
use BlackOps\StorageProtection\StorageKey;
use BlackOps\StorageProtection\StorageKeyProvider;
use BlackOps\StorageProtection\StoragePurpose;
use InvalidArgumentException;

final readonly class SampleStorageKeyProvider implements StorageKeyProvider
{
    private const string KEY_ID = 'quickstart';

    private StorageKey $key;

    public function __construct()
    {
        $encoded = $_ENV['BLACKOPS_STORAGE_KEY'] ?? null;
        if (!is_string($encoded) || $encoded === '') {
            throw new InvalidArgumentException('BLACKOPS_STORAGE_KEY must be strict base64 for exactly 32 bytes.');
        }
        $material = base64_decode($encoded, strict: true);
        if (!is_string($material) || strlen($material) !== SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
            throw new InvalidArgumentException('BLACKOPS_STORAGE_KEY must be strict base64 for exactly 32 bytes.');
        }
        $this->key = new StorageKey(self::KEY_ID, $material);
    }

    public function activeKey(?TenantRef $tenant, StoragePurpose $purpose): StorageKey
    {
        return $this->key;
    }

    public function key(string $keyId, ?TenantRef $tenant, StoragePurpose $purpose): StorageKey
    {
        if ($keyId !== self::KEY_ID) {
            throw new InvalidArgumentException('Unknown storage key identifier.');
        }
        return $this->key;
    }
}

このLocal ProviderのBLACKOPS_STORAGE_KEY取得箇所をProductionのSecret Manager/KMS adapterへ置き換えてください。Vendor固有の接続、Key生成、保存、失効はFrameworkの範囲外です。StorageKeyのConstructorがKey IDと32-byte長を検証し、Unknown Key IDは安全な例外へ縮約します。Secret Sourceの応答、Key ID、Tenant Raw ID、MaterialをLog、Artifact、Exception、Reportへ出しません。

activeKey()は新規Write用、key()はEnvelope HeaderのKey IDを読むためだけに使います。Key MaterialはRepository、.env Snapshot、Compiled Container、Manifest、Exception、Log、Journal、CLI、Reportへ保存しません。Build Artifactに残るのはProvider Class Reference/Binding Metadataだけです。

BindingをBuildし、3入口で同じTenantを実行する

上のApplicationServiceProviderをApplicationのServiceとして登録します。ServiceRegistryで使える登録はautowire()set()だけです。TenantRefやKey Materialをset()へ渡さず、値を解決するApplication-owned Resolver/Providerをautowire()してください。

// config/app.php
return [
    'services' => [
        App\ApplicationServiceProvider::class,
    ],
];

Application Commandservices配列へ既存登録と一緒に追加し、次の順でArtifactを作ります。Application-owned StorageKeyProviderOperationDataReadAuthorizerOperationStatusAuthorizerの実装手順も同じTenant and Protected Storageにあります。

php blackops build:compile
php blackops list

次に、Applicationが既に持つ#[Route]付きHTTP Operation、Console Command、Scheduled Operationを同じTenant解決設定で実行します。固定Route、Fixture、Credential、Tenant IDはFrameworkが提供しないため発明せず、各正本の例へ置き換えてください。6. HTTPで受け付ける3. Human/JSONで実行するMigration、Build、初回実行を順番に参照します。

# First Operationの既存HTTP Routeへ検証済みCredentialを渡す(HTTP 202/`status: "accepted"`を確認)
curl -i -X POST -H 'Content-Type: application/json' \
  -H 'X-Sample-Token: local-example' \
  -d '{"customerName":"Acme","email":"billing@example.com","quantity":2,"billingReference":"PO-2026-001"}' \
  http://127.0.0.1:8080/invoices

# Console Commandの既存Sampleを実行する(Exit 0、HumanのCompletedまたはJSONの`status: "completed"`を確認)
php blackops report:export --report-name=weekly --json

# Schedule ContextからTenantを解決する既存Scheduleを一回評価する(Exit 0、JSONの`status: "ok"`とCountを確認)
php blackops operation:schedule:run --json

HTTPはAuthenticatorが返したTenant、ConsoleはApplicationConsoleTenantProvider、ScheduledはApplicationScheduledTenantProviderが返したTenantをRoot ExecutionContextへ設定します。HTTPの期待JSONは{"status":"accepted","operationId":"<operation-id>","acceptedAt":"<timestamp>"}です。ConsoleはExit 0status: "completed"、ScheduledはExit 0status: "ok"evaluatedaccepted Countを確認します。受付または評価されたOperationのContext、Journal、Status/Outcome、Deferred/Outbox/Retryへ同じTenantRefが伝播することも確認します。Providerが未登録または解決に失敗した場合は安全なConfiguration/Authorization Errorで停止し、別TenantやTenantなしへFallbackしません。

3. Fresh Databaseを起動する

同じReleaseのArtifact、Database/Schema、Provider BindingをHTTP、Worker、Console、Scheduled、Outbox Relayへ渡します。Install後は暗黙のDDLやBuildに頼らず、次の順で実行します。

php blackops database:migrate
php blackops build:compile
php blackops database:seed

旧保護対象Tableが非空なら、Migrationは内容を検査せず、変更(ALTER/変換)前にSafe Migration Errorで停止します。空Tableだけが保護Schemaへ進めます。欠落Header、Malformed、Unknown Key、Tampered TagはMigrationの検出対象ではなく、Runtime Protection Failureとしてだけ扱います。どちらの境界でもPayload、Row内容、Key Materialを出力しません。

4. Status/Journal/Outcomeを認可後に読む

Statusの既定AuthorizerはDenyです。Status QueryはClear SubjectをTenant Predicate付きで先に読み、Allow後だけDetail/OutcomeのProtected BlobをDecodeします。UnknownとDenyは同じUnavailable/404で、Operation IDはSecretではありません。

ApplicationがStatus PolicyをBindingしたうえで、End-userのTyped Outcomeは既存Status Resource(GET /operations/{operationId}、Generated Clientの.status().wait())から取得します。Raw Canonical JournalをPublic HTTPへ公開しません。

PHP AdapterからJournal/Outcomeを読む場合も、Raw ReaderではなくDefault-deny Queryを使います。

use BlackOps\Core\Identifier\OperationId;
use BlackOps\OperationData\OperationDataPurpose;
use BlackOps\OperationData\OperationOutcomeFound;
use BlackOps\OperationData\OperationOutcomeQuery;

$result = $outcomes->find(
    OperationId::fromString($operationId),
    $currentActor,
    $currentTenant,
    OperationDataPurpose::fromString('support.read'),
);

if ($result instanceof OperationOutcomeFound) {
    $record = $result->record();
}

OperationJournalQuery::records()OperationOutcomeQuery::find()は、Operation ID、Current Actor、Current Tenant、Application-owned OperationDataPurposeを受け取ります。Deny、Tenant不一致、Unknown、Retention削除はOperationJournalUnavailableOperationOutcomeUnavailableです。Allow前にPayload/Outcome/CredentialをDecodeしません。

5. DatabaseにRaw Valueがないことを確認する

保護対象はCanonical Journal、Deferred Payload/Context、Outcome Payload、Outbox Payload/Context、Dead Letter Reason、Idempotency Response/Resultです。Queryに必要なTenant、Operation、State、Sequence、Timestamp、Schema Versionなどの最小MetadataだけがClear Columnに残ります。Key IDはClear ColumnではなくBOPD Envelope Headerにあります。

FrameworkのEnvelopeはBOPD v1(XChaCha20-Poly1305)です。HeaderはVersion/Algorithm/Key ID/Nonce/Ciphertext/Authentication Tagを持ち、AADはStorage Purpose、Record/Operation Identity、Schema Version、Tenantの有無・Type・IDへ結び付きます。別Row、別Field、別Purpose、別TenantのCiphertext差し替えはTag検証で拒否されます。

ApplicationのRead-only DB確認では本文を表示せず、列の存在とBOPD Prefixだけを数える全範囲の集計です。これはBounded Scanではありません。接続先をApplicationのSchemaへ置き換え、Read-only RoleのTransactionで実行し、CredentialをCommandへ直書きしません。

WITH checks AS (
    SELECT 'journal' AS table_name, 'encoded_record' AS column_name,
           count(encoded_record) AS non_null_rows,
           count(encoded_record) FILTER (WHERE substring(encoded_record FROM 1 FOR 4) = decode('424f5044', 'hex')) AS bopd_rows
    FROM blackops.journal
    UNION ALL SELECT 'operations', 'encoded_payload', count(encoded_payload), count(encoded_payload) FILTER (WHERE substring(encoded_payload FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.operations
    UNION ALL SELECT 'operations', 'encoded_context', count(encoded_context), count(encoded_context) FILTER (WHERE substring(encoded_context FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.operations
    UNION ALL SELECT 'outcomes', 'encoded_payload', count(encoded_payload), count(encoded_payload) FILTER (WHERE substring(encoded_payload FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.outcomes
    UNION ALL SELECT 'outbox_records', 'encoded_payload', count(encoded_payload), count(encoded_payload) FILTER (WHERE substring(encoded_payload FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.outbox_records
    UNION ALL SELECT 'outbox_records', 'encoded_context', count(encoded_context), count(encoded_context) FILTER (WHERE substring(encoded_context FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.outbox_records
    UNION ALL SELECT 'dead_letters', 'encoded_reason', count(encoded_reason), count(encoded_reason) FILTER (WHERE substring(encoded_reason FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.dead_letters
    UNION ALL SELECT 'idempotency_records', 'encoded_response', count(encoded_response), count(encoded_response) FILTER (WHERE substring(encoded_response FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.idempotency_records
    UNION ALL SELECT 'idempotency_records', 'encoded_result', count(encoded_result), count(encoded_result) FILTER (WHERE substring(encoded_result FROM 1 FOR 4) = decode('424f5044', 'hex')) FROM blackops.idempotency_records
)
SELECT table_name, column_name, non_null_rows, bopd_rows,
       (non_null_rows = bopd_rows) AS all_non_null_rows_are_bopd
FROM checks
ORDER BY table_name, column_name;

接続ContextはApplicationのconfig/database.phpで指定したFramework Schema(既定blackops)とRead-only Roleです。Nullable列ではcount(column)がnon-null行だけを数えます。空Tableはnon_null_rows=0bopd_rows=0となり、確認対象のDataがないため一致と解釈します。一致条件は各行のnon_null_rows = bopd_rowsです。結果はTable、Column、件数、BOPD一致Booleanだけで、値、Outcome、Reason、Response、Nonce、Tag、Tenant Raw IDを返しません。

同じ形で、本文列を選択せずに9 Purposeの全Storageを確認します。

Storage Purpose Protected column(Prefixだけを数える)
journal_record journal.encoded_record
deferred_payload operations.encoded_payload
deferred_context operations.encoded_context
outcome_payload outcomes.encoded_payload
outbox_payload outbox_records.encoded_payload
outbox_context outbox_records.encoded_context
dead_letter_reason dead_letters.encoded_reason
idempotency_response idempotency_records.encoded_response
idempotency_result idempotency_records.encoded_result

この集計はcount(column)とBOPD Prefixのcount(column) FILTER (...)だけを返します。convert_from、JSON展開、復号、Payload/Outcome/Reason/Responseの選択は行いません。Malformed/Unknown/Tampered EnvelopeはMigration検出ではなく、RuntimeのProtection Failureとして安全に拒否されます。

Payload、Outcome、Reason、Response、Nonce、Tag、Tenant Raw IDをSELECT、Log、Support Ticket、Screenshotへ出さないでください。Disk/Volume/Managed Database EncryptionはEnvelopeの代替ではなく追加層です。

6. Rotationを安全に完走する

RotationはProviderのActive Write Key切替と既存EnvelopeのRe-encryptを分けます。まず旧Keyと新KeyをRead可能にし、Replica/Backup/Dead Letter/Retention Windowの確認責任を運用側に残します。旧Keyを先に削除しません。

Plan(Read-only)

php blackops storage:protection:plan \
  --purpose=journal_record \
  --old-key-id=old:v1 \
  --new-key-id=new:v1 \
  --batch=100 \
  --checkpoint=journal-v1-to-v2 \
  --json

--purposeは9つのStoragePurposeのいずれか、Tenant Scopeは--tenant-type--tenant-idを両方指定するか省略します。Key IDは異なる必要があり、--batchは1〜1000(既定100)、Checkpointは小文字の識別子です。PlanはHeader/Clear Metadataだけを読むRead-only集計で、Write、Decode、Audit更新を行いません。Batchを超えるRowを一度に変更しないBounded処理はConfirm時のRotationだけです。

Rotate(Dry-run/Confirm)

storage:protection:rotateは既定でPlan相当のDry-runです。変更には--confirm、明示的な--checkpoint、非空の--actor--reasonが必要です。--dry-runを付けた場合はConfirmより優先され、bytesは変わりません。

php blackops storage:protection:rotate \
  --purpose=journal_record \
  --old-key-id=old:v1 \
  --new-key-id=new:v1 \
  --batch=100 \
  --checkpoint=journal-v1-to-v2 \
  --actor=maintenance \
  --reason='scheduled key migration' \
  --confirm --json

JSON/Human出力はpurpose、Key ID、Checkpoint、selectedrotatedskippedfailed、Key別remainingstateだけを返します。remainingBoundarydatabase-current-scopeは、Replica、Backup、Dead Letter scope、Retention windowを別途確認する必要があることを示します。

Exit Codeは成功(0)、入力/確認/設定Error(2)、Storage/Protection/Runtime Failure(1)です。CASで競合RowをSkipし、CheckpointとAuditを同じTransactionで進めます。CrashやSIGKILL後は同じScope/Checkpointで再実行し、remainingが0になるまでBounded Batchを繰り返します。Failed Auditはsafe fingerprintだけを記録し、修復後に再開します。

7. Retention、Replay、Outbox、Idempotency

RetentionはClear Tenant、Operation ID、State、Timestamp、HoldだけでPlan/Purgeし、CiphertextをDecodeしません。Observer ReplayはInfrastructure AuthorityとしてRowごとのTenant/Protection Contextで復号し、現在のSensitive Projection後だけをObserverへ渡します。Outbox、Dead Letter、Idempotencyも同じBOPD保護とTenant/AAD境界を使います。

Troubleshooting

症状 安全な確認 次の判断
Provider未登録でBootstrap停止 Service ProviderのStorageKeyProvider Bindingだけを確認 KeyをArtifactへ埋め込まずProviderを登録
Unknown Key/Tag不一致 Safe Protection Errorと固定Fingerprintだけを確認 旧KeyをRead可能にし、Envelope/AAD Scopeを修復
Tenant付きStatusが404 Current/Origin TenantとStatus Authorizer Bindingを確認 Tenant IDをCredentialやHeaderから推測しない
Upgradeが旧Plaintextで停止 MigrationのSafe Errorと空/非空判定だけを確認 Reset/Recreateまたは承認済みOffline変換
Rotation後remainingが0にならない Purpose、Tenant Scope、Old Key ID、Checkpointを照合 Replica/Backup等の別境界も確認し、旧Keyを削除しない

どのFailureでもPayloadやKeyを公開せず、表のSafe確認で原因を切り分けてProvider/Policy/Migrationを修正し、同じScopeのPlanまたは確認済みRuntimeから再実行します。

Provider未登録、Unknown Key/Tag Tamper、非空旧Schema、remaining未解消の具体的な手順は、StorageKeyProviderが未登録Unknown Key/Tag Tamper非空の旧Protected SchemaでMigrationが停止Rotationのremainingが0にならないを参照してください。その他の運用はSecurityDeploymentTroubleshootingReleasesを参照してください。

次に観測結果を安全に送る

Protected Dataを含む実行を観測する場合は、ObservabilityでProvider、Health、Collectorの境界を確認します。