Skip to content

feat(secrets): add secret rotation reminders and auto-rotation#43646

Closed
akoscz wants to merge 2 commits into
openclaw:mainfrom
akoscz:feature/secrets-rotation
Closed

feat(secrets): add secret rotation reminders and auto-rotation#43646
akoscz wants to merge 2 commits into
openclaw:mainfrom
akoscz:feature/secrets-rotation

Conversation

@akoscz

@akoscz akoscz commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two complementary systems for managing secret lifecycle, plus the openclaw secrets CLI command.

RotationReminders (rotation-reminders.ts)

Track and enforce secret rotation schedules:

  • Labels stored as KV metadata in config (rotate-every-days, last-rotated-at)
  • Check if any secrets are overdue: openclaw secrets check-rotation
  • Snooze or acknowledge rotation reminders
  • Per-secret interval configuration

AutoRotation (auto-rotation.ts)

Automatic gateway token rotation via GCP Secret Manager:

  • Creates a new random token, writes to GCP Secret Manager, updates local config
  • Configurable interval (default: 30 days)
  • Dry-run mode for testing: openclaw secrets rotate --dry-run

CLI Command (src/commands/secrets.ts)

Command Description
openclaw secrets list List all configured providers and their secrets
openclaw secrets test Test provider connectivity
openclaw secrets rotate Rotate the gateway token
openclaw secrets check-rotation Check rotation status
openclaw secrets purge Remove plaintext secrets from config (after migrating to provider refs)

Notes

Split from #24272 per @vincentkoc's request.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime commands Command implementations size: XL labels Mar 12, 2026
@greptile-apps

greptile-apps Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces two new modules (rotation-reminders.ts and auto-rotation.ts) plus the openclaw secrets CLI command for managing secret lifecycle — rotation reminders, GCP-backed auto-rotation of the gateway token, and supporting subcommands. The design is solid: dependency injection for testability, immutable metadata mutations, and a TDD test suite with good coverage.

Issues found:

  • src/config/rotation-reminders.ts line ~198emitRotationEvents emits the wrong event name in the expired branch; any consumer listening for the expired event will never receive it, silently mis-classifying expired credentials as expiring-soon.
  • src/config/auto-rotation.ts line ~203rotateGatewayToken verifies versions/latest instead of the specific newly-created version (versionName), creating a race condition if two rotation jobs run concurrently.
  • src/config/auto-rotation.ts line ~244createDefaultDeps hardcodes project: "n30-agents", which appears to be a staging/test value; any caller who doesn't override it will write tokens to the wrong GCP project.
  • src/commands/secrets.ts line ~226secretsSetupAws builds an IAM policy ARN with * as the account ID, which is not a valid ARN and will cause the aws iam attach-user-policy command to fail at runtime.
  • src/commands/secrets.ts line ~97scanForSensitiveValues only skips values already using ${gcp:...} syntax; references to other providers (e.g. ${aws:...}) would be incorrectly flagged as unmitigated plaintext.

Confidence Score: 2/5

  • Not safe to merge — contains a hardcoded GCP project name that would misdirect token writes, an invalid AWS IAM ARN that breaks setup, and an incorrect event name that silently swallows expired-credential notifications.
  • Three of the four logic bugs would silently produce incorrect runtime behavior: the wrong GCP project receives rotated tokens, the AWS setup command fails with an invalid ARN, and expired credentials are never surfaced through the correct event channel. The verification race condition adds additional correctness risk for the core rotation flow.
  • src/config/auto-rotation.ts (hardcoded project + verification race condition) and src/commands/secrets.ts (invalid AWS ARN) need the most attention before merging.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/auto-rotation.ts
Line: 203-207

Comment:
**Verification reads `versions/latest` instead of the newly stored version**

After writing the new secret version, the verification step reads `versions/latest` rather than the specific `versionName` returned by `addSecretVersion`. In a concurrent environment (e.g., two rotation jobs running in parallel), a second write could land between the `addSecretVersion` call and this `accessSecretVersion` call, causing the verification to confirm the wrong version while the config update proceeds with the original `newToken`.

```suggestion
    await client.accessSecretVersion({
      name: versionName,
    });
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/auto-rotation.ts
Line: 244-247

Comment:
**Hardcoded GCP project `"n30-agents"` leaks into production defaults**

`createDefaultDeps` ships `project: "n30-agents"` as the default. Any user who calls this function without overriding `project` will silently write new secret versions to `n30-agents` instead of their own GCP project. This value looks like a test/staging project that was never replaced before shipping.

The project should be a required field (no default), or at minimum read from the loaded `openclaw.json` config at call time rather than being baked into the factory.

```suggestion
export function createDefaultDeps(overrides?: Partial<RotationDeps>): RotationDeps {
  if (!overrides?.project) {
    throw new Error("project is required in RotationDeps — set it via openclaw.json secrets.providers.gcp.project");
  }
  return {
    project: overrides.project,
    secretName: "openclaw-main-gateway-token",
    configPath: `${process.env.HOME}/.openclaw/openclaw.json`,
    intervalDays: 30,
    readConfig: defaultReadConfig,
    writeConfig: defaultWriteConfig,
    getClient: defaultGetClient,
    ...overrides,
  };
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/commands/secrets.ts
Line: 226-229

Comment:
**Wildcard `*` in IAM policy ARN is not valid**

`arn:aws:iam::*:policy/${policyName}` uses `*` for the account ID, which is not a valid ARN for `aws iam attach-user-policy`. AWS requires the exact 12-digit account ID. This command will fail at runtime for any user.

The account ID should be extracted from the `aws sts get-caller-identity` response that was already called a few lines above:

```typescript
const callerIdentity = JSON.parse(stsCheck.stdout) as { Account: string };
const accountId = callerIdentity.Account;
// ...
await exec(
  `aws iam attach-user-policy --user-name openclaw-${agent} --policy-arn arn:aws:iam::${accountId}:policy/${policyName}`,
);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/commands/secrets.ts
Line: 97-98

Comment:
**`scanForSensitiveValues` only skips `${gcp:}` references**

The early-return guard only checks for `${gcp:` prefix. If a user has already migrated some secrets to AWS (`${aws:...}`) or Vault (`${vault:...}`), those values would still be scanned and reported as plaintext secrets to migrate. This would produce false positives in `openclaw secrets migrate`.

```suggestion
    if (obj.match(/^\$\{[a-z]+:/)) {
      return results;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/rotation-reminders.ts
Line: 198-203

Comment:
**Wrong event emitted for the `expired` rotation state**

The `expired` case in `emitRotationEvents` calls the listener using the same event name as the `expiring-soon` case. Any consumer that registers a handler specifically for the `expired` event will never receive it — fully expired credentials will silently appear to be merely expiring-soon. The event name passed to the listener in this branch should be `"secret:" + "expired"` to match the actual state, consistent with how the other cases are handled.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 04b82c3

Comment on lines +203 to +207
try {
await client.accessSecretVersion({
name: `projects/${project}/secrets/${secretName}/versions/latest`,
});
} catch (err: unknown) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification reads versions/latest instead of the newly stored version

After writing the new secret version, the verification step reads versions/latest rather than the specific versionName returned by addSecretVersion. In a concurrent environment (e.g., two rotation jobs running in parallel), a second write could land between the addSecretVersion call and this accessSecretVersion call, causing the verification to confirm the wrong version while the config update proceeds with the original newToken.

Suggested change
try {
await client.accessSecretVersion({
name: `projects/${project}/secrets/${secretName}/versions/latest`,
});
} catch (err: unknown) {
await client.accessSecretVersion({
name: versionName,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/auto-rotation.ts
Line: 203-207

Comment:
**Verification reads `versions/latest` instead of the newly stored version**

After writing the new secret version, the verification step reads `versions/latest` rather than the specific `versionName` returned by `addSecretVersion`. In a concurrent environment (e.g., two rotation jobs running in parallel), a second write could land between the `addSecretVersion` call and this `accessSecretVersion` call, causing the verification to confirm the wrong version while the config update proceeds with the original `newToken`.

```suggestion
    await client.accessSecretVersion({
      name: versionName,
    });
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +244 to +247
export function createDefaultDeps(overrides?: Partial<RotationDeps>): RotationDeps {
return {
project: "n30-agents",
secretName: "openclaw-main-gateway-token",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded GCP project "n30-agents" leaks into production defaults

createDefaultDeps ships project: "n30-agents" as the default. Any user who calls this function without overriding project will silently write new secret versions to n30-agents instead of their own GCP project. This value looks like a test/staging project that was never replaced before shipping.

The project should be a required field (no default), or at minimum read from the loaded openclaw.json config at call time rather than being baked into the factory.

Suggested change
export function createDefaultDeps(overrides?: Partial<RotationDeps>): RotationDeps {
return {
project: "n30-agents",
secretName: "openclaw-main-gateway-token",
export function createDefaultDeps(overrides?: Partial<RotationDeps>): RotationDeps {
if (!overrides?.project) {
throw new Error("project is required in RotationDeps — set it via openclaw.json secrets.providers.gcp.project");
}
return {
project: overrides.project,
secretName: "openclaw-main-gateway-token",
configPath: `${process.env.HOME}/.openclaw/openclaw.json`,
intervalDays: 30,
readConfig: defaultReadConfig,
writeConfig: defaultWriteConfig,
getClient: defaultGetClient,
...overrides,
};
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/auto-rotation.ts
Line: 244-247

Comment:
**Hardcoded GCP project `"n30-agents"` leaks into production defaults**

`createDefaultDeps` ships `project: "n30-agents"` as the default. Any user who calls this function without overriding `project` will silently write new secret versions to `n30-agents` instead of their own GCP project. This value looks like a test/staging project that was never replaced before shipping.

The project should be a required field (no default), or at minimum read from the loaded `openclaw.json` config at call time rather than being baked into the factory.

```suggestion
export function createDefaultDeps(overrides?: Partial<RotationDeps>): RotationDeps {
  if (!overrides?.project) {
    throw new Error("project is required in RotationDeps — set it via openclaw.json secrets.providers.gcp.project");
  }
  return {
    project: overrides.project,
    secretName: "openclaw-main-gateway-token",
    configPath: `${process.env.HOME}/.openclaw/openclaw.json`,
    intervalDays: 30,
    readConfig: defaultReadConfig,
    writeConfig: defaultWriteConfig,
    getClient: defaultGetClient,
    ...overrides,
  };
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/commands/secrets.ts
Comment on lines +226 to +229
}

// 2. Verify credentials
const stsCheck = await exec("aws sts get-caller-identity");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard * in IAM policy ARN is not valid

arn:aws:iam::*:policy/${policyName} uses * for the account ID, which is not a valid ARN for aws iam attach-user-policy. AWS requires the exact 12-digit account ID. This command will fail at runtime for any user.

The account ID should be extracted from the aws sts get-caller-identity response that was already called a few lines above:

const callerIdentity = JSON.parse(stsCheck.stdout) as { Account: string };
const accountId = callerIdentity.Account;
// ...
await exec(
  `aws iam attach-user-policy --user-name openclaw-${agent} --policy-arn arn:aws:iam::${accountId}:policy/${policyName}`,
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/secrets.ts
Line: 226-229

Comment:
**Wildcard `*` in IAM policy ARN is not valid**

`arn:aws:iam::*:policy/${policyName}` uses `*` for the account ID, which is not a valid ARN for `aws iam attach-user-policy`. AWS requires the exact 12-digit account ID. This command will fail at runtime for any user.

The account ID should be extracted from the `aws sts get-caller-identity` response that was already called a few lines above:

```typescript
const callerIdentity = JSON.parse(stsCheck.stdout) as { Account: string };
const accountId = callerIdentity.Account;
// ...
await exec(
  `aws iam attach-user-policy --user-name openclaw-${agent} --policy-arn arn:aws:iam::${accountId}:policy/${policyName}`,
);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/commands/secrets.ts
Comment on lines +97 to +98
/** Scan config for values that look like secrets (API keys, tokens, etc.) */
function scanForSensitiveValues(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanForSensitiveValues only skips ${gcp:} references

The early-return guard only checks for ${gcp: prefix. If a user has already migrated some secrets to AWS (${aws:...}) or Vault (${vault:...}), those values would still be scanned and reported as plaintext secrets to migrate. This would produce false positives in openclaw secrets migrate.

Suggested change
/** Scan config for values that look like secrets (API keys, tokens, etc.) */
function scanForSensitiveValues(
if (obj.match(/^\$\{[a-z]+:/)) {
return results;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/secrets.ts
Line: 97-98

Comment:
**`scanForSensitiveValues` only skips `${gcp:}` references**

The early-return guard only checks for `${gcp:` prefix. If a user has already migrated some secrets to AWS (`${aws:...}`) or Vault (`${vault:...}`), those values would still be scanned and reported as plaintext secrets to migrate. This would produce false positives in `openclaw secrets migrate`.

```suggestion
    if (obj.match(/^\$\{[a-z]+:/)) {
      return results;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +198 to +203
break;
}
}
}

/** Emit auth failure event (called externally when 401/403 detected) */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong event emitted for the expired rotation state

The expired case in emitRotationEvents calls the listener using the same event name as the expiring-soon case. Any consumer that registers a handler specifically for the expired event will never receive it — fully expired credentials will silently appear to be merely expiring-soon. The event name passed to the listener in this branch should be "secret:" + "expired" to match the actual state, consistent with how the other cases are handled.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/rotation-reminders.ts
Line: 198-203

Comment:
**Wrong event emitted for the `expired` rotation state**

The `expired` case in `emitRotationEvents` calls the listener using the same event name as the `expiring-soon` case. Any consumer that registers a handler specifically for the `expired` event will never receive it — fully expired credentials will silently appear to be merely expiring-soon. The event name passed to the listener in this branch should be `"secret:" + "expired"` to match the actual state, consistent with how the other cases are handled.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04b82c30f6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


export function createDefaultDeps(overrides?: Partial<RotationDeps>): RotationDeps {
return {
project: "n30-agents",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make rotation project configurable

createDefaultDeps hard-codes project: "n30-agents", and secretsRotateCommand does not expose a project override in its public options, so rotations always target that single GCP project. In any deployment using a different project this either fails outright or rotates the wrong secret, which makes the new rotation command unusable outside the maintainer environment.

Useful? React with 👍 / 👎.

Comment thread src/commands/secrets.ts
Comment on lines +195 to +199
await exec(
`gcloud iam service-accounts create openclaw-${agent} --display-name="OpenClaw ${agent} agent" --project=${options.project}`,
);
await exec(
`gcloud projects add-iam-policy-binding ${options.project} --member=serviceAccount:openclaw-${agent}@${options.project}.iam.gserviceaccount.com --role=roles/secretmanager.secretAccessor`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail setup when provisioning commands return non-zero

The setup flow runs critical provisioning commands (gcloud iam service-accounts create, IAM policy binding, and similarly in the AWS branch) without checking exitCode, then still returns success and writes config. If permissions/API setup fail, users are told setup succeeded even though runtime secret access is broken; each provisioning step should be validated and abort on failure.

Useful? React with 👍 / 👎.

Comment thread src/commands/secrets.ts
Comment on lines +173 to +175
const exec = options._mockExec;
if (!exec) {
return 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provide a non-mock execution path for secrets setup

This command exits with code 1 whenever _mockExec is not injected, which means the implementation currently has no production path to run real setup logic. The same _mock* pattern is used across the new secrets command functions, so invoking these commands outside tests cannot perform the promised operations.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-rotation branch from 04b82c3 to 1de09fc Compare March 12, 2026 15:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1de09fc777

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/commands/secrets.ts
Comment on lines +328 to +330
// 5. Purge
if (options._mockPurge) {
options._mockPurge();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rewrite config to secret refs before purging

The migrate flow uploads detected secrets and then proceeds to purge plaintext values, but it never updates the loaded config to replace those plaintext fields with SecretRef values (for example ${gcp:...}) before purge. In any run where purge is enabled, this can remove the only usable credentials while leaving config entries unresolved, which turns a successful migration into broken auth at runtime. Add an explicit rewrite-and-save step before purge and fail the command if that rewrite cannot be completed.

Useful? React with 👍 / 👎.

Comment on lines +132 to +133
token: newToken,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve gateway token SecretRef during rotation

This always writes the freshly generated token value directly into gateway.auth.token. When users are configured with external secret refs (for example gateway.auth.token: "${gcp:openclaw-main-gateway-token}"), running rotation replaces that reference with plaintext in openclaw.json, which defeats the security model and stops future rotations from being centrally managed by the provider. The update path should preserve/refill the existing ref instead of persisting the raw token.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-rotation branch from 1de09fc to 87b8e4a Compare March 12, 2026 20:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87b8e4a4d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

} = deps;

// 1. Read current config to get old token
const config = (await readConfig(configPath)) as Record<string, unknown> | null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle config/client init failures in token rotation

rotateGatewayToken returns a RotationResult, but both readConfig(configPath) and getClient() are awaited outside any error handling. If the config file is missing/invalid JSON or the Secret Manager client cannot initialize, this throws and bypasses the function’s structured failure path, so callers like secretsRotateCommand cannot report a controlled rotation failure.

Useful? React with 👍 / 👎.

Comment thread src/commands/secrets.ts
return 1;
}

await exec(`gcloud services enable secretmanager.googleapis.com --project=${options.project}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require project before building gcloud setup commands

options.project is optional, but the GCP setup path interpolates it directly into every provisioning command. When --project is omitted, commands are emitted with --project=undefined (and IAM principals containing @undefined), so setup targets an invalid project and persists an unusable provider config instead of failing fast.

Useful? React with 👍 / 👎.

return {
project: "n30-agents",
secretName: "openclaw-main-gateway-token",
configPath: `${process.env.HOME}/.openclaw/openclaw.json`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive default config path from homedir, not HOME only

The default rotation deps build configPath from process.env.HOME only. In environments where HOME is unset (for example some Windows/service contexts), this becomes undefined/.openclaw/openclaw.json, causing rotation to fail before it can read config. Use os.homedir() (or an existing path helper) for a portable default.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-rotation branch from 3ffd48f to 06487e1 Compare March 12, 2026 22:55
@openclaw-barnacle openclaw-barnacle Bot added the scripts Repository scripts label Mar 12, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06487e14c6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/config/secret-resolution.ts Outdated
Comment on lines +317 to +320
if (name === "gcp" && config && config.project) {
providers.set(
"gcp",
new GcpSecretProvider(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register AWS provider in buildSecretProviders

buildSecretProviders currently only instantiates gcp and env, so an aws entry under secrets.providers is silently ignored. That makes ${aws:...} references fail with UnknownSecretProviderError even when AWS is configured, which means the new AWS provider path is unusable in normal runtime resolution.

Useful? React with 👍 / 👎.

Comment thread src/config/secret-resolution.ts Outdated
version: string | undefined,
cacheTtlMs: number,
): Promise<string> {
const cacheKey = `${provider.name}:${name}#${version ?? "latest"}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope shared cache keys by provider config

The shared cache key is only ${provider.name}:${secret}#${version}, so it does not distinguish different GCP projects or AWS accounts/regions using the same provider alias. In a long-lived process that resolves multiple configs, a secret fetched for one tenant can be returned for another tenant with the same name, producing incorrect values and cross-tenant secret leakage until the TTL expires.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-rotation branch from 06487e1 to 91aa883 Compare March 13, 2026 02:01
@openclaw-barnacle openclaw-barnacle Bot removed docs Improvements or additions to documentation gateway Gateway runtime scripts Repository scripts labels Mar 13, 2026
@akoscz akoscz force-pushed the feature/secrets-rotation branch from 91aa883 to a7c7fc0 Compare March 13, 2026 02:05
Adds two complementary systems for managing secret lifecycle:

**RotationReminders** (`rotation-reminders.ts`):
- Track rotation schedules via KV metadata labels in config
- Check if secrets are due for rotation
- Snooze and acknowledge rotation reminders
- CLI: `openclaw secrets check-rotation`

**AutoRotation** (`auto-rotation.ts`):
- Automatically rotate the OpenClaw gateway token in GCP Secret Manager
- Configurable rotation interval (default: 30 days)
- Dry-run mode for testing
- CLI: `openclaw secrets rotate`

**CLI command** (`src/commands/secrets`):
- `openclaw secrets list` — list all configured providers and their secrets
- `openclaw secrets test` — test provider connectivity
- `openclaw secrets rotate` — rotate gateway token
- `openclaw secrets check-rotation` — check rotation status
- `openclaw secrets purge` — remove plaintext secrets from config

Split from openclaw#24272 per @vincentkoc's request.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7c7fc09a2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

oldToken,
newToken,
versionName,
error: `Failed to update local config (old token: ${oldToken}): ${err instanceof Error ? err.message : String(err)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove old token value from rotation error text

When rotateGatewayToken hits a config write failure (for example, permission denied on openclaw.json), it embeds oldToken directly into the returned error string, and secretsRotateCommand prints that string to stderr. This leaks a live gateway credential into terminal history/CI logs in a failure path, which is a security regression even when rotation itself fails.

Useful? React with 👍 / 👎.

Comment thread src/commands/secrets.ts

if (typeof obj === "string") {
// Skip if already a secret ref
if (obj.includes("${gcp:")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat all SecretRef templates as already-migrated values

scanForSensitiveValues only skips strings containing ${gcp:, so other reference formats (e.g. ${aws:...} or ${env:...} with long IDs) still satisfy the secret heuristic and get re-uploaded as literal text during secretsMigrateCommand. In configs that already use non-GCP refs, migration will incorrectly process those refs as plaintext secrets and can lead to bad uploads (and risky purge behavior if enabled).

Useful? React with 👍 / 👎.

The rotation branch directly imports @google-cloud/secret-manager for
auto-rotation. CI tests each PR against origin/main independently, so
without the declaration the package is not installed during type-check
and tests, causing TS2307 errors.

Added as optional peerDependency to match the pattern used by the
base secrets provider branches.
@akoscz akoscz force-pushed the feature/secrets-rotation branch from a7c7fc0 to 7e64931 Compare March 13, 2026 05:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e649314a6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/commands/secrets.ts
return 0;
}

export async function secretsSetupCommand(options: SetupCommandOptions): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire new secrets subcommands into CLI registration

The handlers added in this file are not reachable from the actual CLI entrypoints: in this commit, registerSecretsCli still only registers reload, audit, configure, and apply (src/cli/secrets-cli.ts), and a repo-wide search shows src/commands/secrets.ts is only imported by its own test file. That means openclaw secrets users cannot invoke the new setup/migrate/rotate/reminder flows in production, so the feature ships as dead code.

Useful? React with 👍 / 👎.

@akoscz

akoscz commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution! After reviewing this PR alongside the related secrets provider PRs (#43640, #43641, #43643, #43644, #43645), we need to close this for an architectural rework.

The issue: GCP-hardcoded rotation outside the existing system

This PR adds rotation reminders and auto-rotation, but the implementation is hardcoded to GCP Secret Manager — auto-rotation.ts calls the GCP client directly. Rotation should be a generic capability that works through the existing src/secrets/ provider system, not a GCP-specific sidecar.

The companion provider PRs this would depend on (#43640, #43641, #43643, #43644, #43645) also need architectural rework themselves — they build a parallel secrets system instead of extending the existing src/secrets/ infrastructure. See the comments on those PRs for details.

What we need instead

Rotation support should live within the existing src/secrets/ provider contract (SecretProviderConfig), making rotation available to any configured provider — not just GCP. src/secrets/runtime.ts and src/gateway/server-methods/secrets.ts are good starting points for understanding how the existing system is structured.

We'd love to see this re-submitted once the provider architecture is sorted out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant