Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions cookbook/aws-secrets-sync.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
---
title: AWS Secrets Sync
description: Sync AWS Secrets Manager values into OpenHands personal or organization secrets.
icon: key
---

{/* GENERATED from OpenHands/enterprise-cookbook@main (aws-secrets-sync/README.md). Edit the source, not this file. */}

<Card title="View source on GitHub" icon="github" href="https://github.com/OpenHands/enterprise-cookbook/tree/main/aws-secrets-sync" horizontal />

A Python script you run on your own scheduler. It reads explicitly mapped AWS
Secrets Manager values, writes them through OpenHands secrets APIs, and updates
them when AWS rotates them. AWS is the working example: you can adapt the source
reader to another secrets manager. Nothing is installed in OpenHands.

## How It Works

```mermaid
sequenceDiagram
participant S as Scheduler
participant P as Sync script
participant J as SQLite journal
participant A as AWS Secrets Manager
participant O as OpenHands API
S->>P: Run with mapping and OH_API_KEY
P->>O: Verify key binding; list secret names
loop Each mapping
P->>J: Read checkpoint
P->>A: Describe AWSCURRENT version
alt Missing or authorized changed version, with --apply
P->>A: Read that exact version
opt Replacing a shared value
P->>J: Record delete_pending
P->>O: DELETE secret
P->>J: Record deleted after acknowledgment
end
P->>J: Record pending
P->>O: POST create or personal upsert
P->>J: Record applied after acknowledgment
end
end
```

The script creates missing entries, skips unchanged AWS versions, and updates
changed versions only for mappings already owned by its journal. Unknown existing
names or edited ARN/field mappings are refused; use a new destination name or
[operator recovery](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/RECOVERY.md). Source deletion and removed mappings never
cause destination deletion.

## Prerequisites

- Python 3.10+ on Linux or macOS; `boto3` and `requests` from `requirements.txt`.
- AWS credentials through the [Boto3 credential chain](https://docs.aws.amazon.com/boto3/latest/guide/credentials.html).
Grant `DescribeSecret` and `GetSecretValue` on the mapped ARNs; see the

Check warning on line 54 in cookbook/aws-secrets-sync.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

cookbook/aws-secrets-sync.mdx#L54

Did you really mean 'ARNs'?
[IAM example](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/iam-policy.example.json) and [AWS access documentation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access.html).
Customer-managed encryption keys may require `kms:Decrypt`.
- An organization-bound OpenHands API key injected as `OH_API_KEY`. Shared-secret
writes require owner/admin permission. Keep credentials out of mappings and
command arguments. `GET /api/keys/current` returns organization and user IDs;
verify they match the intended account and organization before configuring them.

## Run It

From this directory, install dependencies:

```bash
python -m pip install -r requirements.txt
```

Set the mapping's HTTPS origin, organization UUID, exact AWS ARN, and destination

Check warning on line 70 in cookbook/aws-secrets-sync.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

cookbook/aws-secrets-sync.mdx#L70

Did you really mean 'mapping's'?
name (a valid environment-variable name). `json_field` selects one top-level string
field; omit it for the whole string. Binary values are unsupported.

<Tabs>
<Tab title="Organization Secrets">
Copy [mapping.example.json](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/mapping.example.json) to `mapping.local.json` and
edit it. Use the same journal for preview, initial import, and recurring sync.

```bash
python sync_secrets.py --config mapping.local.json --state .sync-state/org/journal.sqlite
```

```text
mapping 1: would create (value not read)
```

```bash
python sync_secrets.py --config mapping.local.json --state .sync-state/org/journal.sqlite --apply
```

```text
mapping 1: create acknowledged
```

After the source version changes, run:

```bash
python sync_secrets.py --config mapping.local.json --state .sync-state/org/journal.sqlite --sync --allow-delete-recreate --apply
```

```text
mapping 1: delete/recreate acknowledged
```

<Warning>
This command deletes and re-creates shared values, including on newer servers.
If creation fails, the secret can remain missing for all members until recovery.
Read [Updates and Recovery](#updates-and-recovery) before scheduling it.
</Warning>
</Tab>

<Tab title="Personal Secrets">
Copy [mapping.personal.example.json](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/mapping.personal.example.json) to
`alice.local.json`. Supply the intended user's own org-bound key and
`expected_user_id`; an administrator key does not impersonate them. Use a separate
mapping and persistent journal for each user.

```bash
python sync_secrets.py --config alice.local.json --state .sync-state/alice/journal.sqlite
```

```text
mapping 1: would create (value not read)
```

```bash
python sync_secrets.py --config alice.local.json --state .sync-state/alice/journal.sqlite --apply
```

```text
mapping 1: create acknowledged
```

After the source version changes, run:

```bash
python sync_secrets.py --config alice.local.json --state .sync-state/alice/journal.sqlite --sync --apply
```

```text
mapping 1: personal upsert acknowledged
```
</Tab>
</Tabs>

Omit `--apply` to preview without reading values. Unchanged versions print:

```text
mapping 1: checkpoint unchanged (not value-verified)
```

Your scheduler can run the recurring command weekly with absolute paths, injected
credentials, and the same persistent journal. Its directory must be private
(`chmod 700`). Run one exclusive writer per destination and alert on any nonzero
exit; successful mappings remain committed when another fails.

## Secret Name Collisions

On the tested API, personal secrets belong to a user within the effective
organization. Personal and shared names are separate namespaces; neither create

Check warning on line 160 in cookbook/aws-secrets-sync.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

cookbook/aws-secrets-sync.mdx#L160

Did you really mean 'namespaces'?
endpoint rejects a name used in the other scope. The personal value keeps the bare
name; the shared value receives a suffix (`_2`, `_3`, …)
in that user's conversations and `GET /api/v1/secrets/search`. The stored shared
name remains unchanged in the organization listing.

<Warning>
An organization sync cannot see members' personal secrets. A member with a
matching personal name keeps their own value under that name; rotating the shared
value only changes their suffixed variable, such as `NAME_2`.
</Warning>

Personal preflight rejects mapped names that match shared names as listed in the
search results. A shared secret created later can appear already suffixed, so the
next sync does **not** detect that base-name collision. Use team/system prefixes
and reserve managed names across scopes; this script cannot enforce that policy.

## Updates and Recovery

Personal values use upsert. This recipe rotates shared values with DELETE then

Check warning on line 179 in cookbook/aws-secrets-sync.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

cookbook/aws-secrets-sync.mdx#L179

Did you really mean 'upsert'?
POST, only with `--sync --allow-delete-recreate`. It validates the new AWS value
before deleting anything. The existing organization metadata PUT cannot replace
values; the newer value-replacement API is separate (see [Compatibility](#compatibility)).

<Warning>
After DELETE succeeds, the shared entry is absent until POST succeeds. New
conversations started in that window lack the shared value. A failed or
unacknowledged POST can leave it missing for all members until an operator
completes [recovery](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/RECOVERY.md); it is not just a brief availability gap.
A timeout can also hide a successful write. The script blocks uncertain mappings
instead of guessing or automatically retrying them.
</Warning>

New conversations receive updated values; running conversations can retain old
values. Credential revocation is a separate process. Keep exclusive control of
managed names: the operations used here have no conditional-write contract, and
the local journal lock cannot coordinate other clients. Out-of-band destination
value changes are not detected.

| Journal Status | Action |
| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| `applied` | Normal sync can continue |
| `deleted` | Acknowledged shared deletion, no POST attempted; rerun the shared sync command to restore an absent entry |
| `pending` / `delete_pending` | Uncertain outcome; follow operator recovery before resuming |

A `deleted` checkpoint also blocks if the entry is unexpectedly present; reconcile
it before proceeding. Do not erase the journal to bypass uncertainty. Values and
tokens are not intentionally logged or stored in it; disable HTTP/SDK wire logging
in your runner.

## Adapt to Another Vault

In [sync_secrets.py](https://github.com/OpenHands/enterprise-cookbook/blob/main/aws-secrets-sync/sync_secrets.py), replace `current_version()` and
`read_value()` with your vault's metadata/version and value API calls. Adjust the
client factory, ARN-to-region parsing in `sync()`, mapping validation, and source
error handling. Keep a stable source ID and version token; read the exact requested
version and fail safely if it changes between metadata and value reads.

Keep the OpenHands adapters and journal for the destination side. Other vaults
are extension points, not included integrations; qualify their authentication,
versioning, retries, and synthetic-secret delivery before use.

## APIs Used

| Endpoint | Used For |
| --------------------------------------------------- | -------------------------------------------------------- |
| AWS `DescribeSecret` / `GetSecretValue` | Locate and read the exact `AWSCURRENT` version |
| `GET /api/keys/current` | Verify effective/bound organization and personal user |
| `GET /api/organizations/{org_id}/secrets` | List shared names |
| `POST /api/organizations/{org_id}/secrets` | Create shared values |
| `DELETE /api/organizations/{org_id}/secrets/{name}` | Remove the old shared entry before re-creation |
| `GET /api/v1/secrets/search` | List personal/shared names visible to the user |
| `POST /api/v1/secrets` | Create or upsert the authenticated user's personal value |

Check warning on line 232 in cookbook/aws-secrets-sync.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

cookbook/aws-secrets-sync.mdx#L232

Did you really mean 'upsert'?

## Compatibility

The [maintainer review](https://github.com/OpenHands/enterprise-cookbook/pull/29#pullrequestreview-5689160630)
validated this script at `2e83432` on **Enterprise 1.74.0**, using simulated AWS
(moto) and real OpenHands APIs and agent consumption of rotated values. Qualify

Check warning on line 238 in cookbook/aws-secrets-sync.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

cookbook/aws-secrets-sync.mdx#L238

Did you really mean 'moto'?
your deployment before importing real credentials; this is not a minimum-version
or blanket compatibility guarantee.

**Enterprise 1.81.0 added atomic shared-value replacement** through separate
`GET .../secrets/{name}/version` and `PUT .../secrets/{name}/value` endpoints.
This recipe does **not** use or qualify them and still deletes/re-creates shared
values on newer servers. Adopting that API requires a separately tested adapter
and conditional-version/retry handling; the ordinary metadata PUT remains
metadata-only. Inspect your deployment's `/openapi.json` before choosing a path.

## Related

<CardGroup cols={2}>
<Card title="OpenHands Secrets Management" href="/openhands/usage/settings/secrets-settings" icon="book-open">
Managing and using secrets
</Card>

<Card title="AWS GetSecretValue" href="https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html" icon="arrow-up-right-from-square">
Exact version reads
</Card>

<Card title="Per-Conversation Secrets" href="/cookbook/per-conversation-secrets" icon="user-secret">
A separate delivery scope
</Card>
</CardGroup>
4 changes: 4 additions & 0 deletions cookbook/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ Observe conversations and react to their state.
Inject credentials and manage identity.

<CardGroup cols={2}>
<Card title="AWS Secrets Sync" icon="key" href="/cookbook/aws-secrets-sync">
Sync AWS Secrets Manager values into OpenHands personal or organization secrets.
</Card>

<Card title="GPG Commit Signing" icon="file-signature" href="/cookbook/gpg-commit-signing">
Configure GPG commit signing on every conversation with a SessionStart hook that imports a key from a custom secret.
</Card>
Expand Down
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@
{
"group": "Secrets & authentication",
"pages": [
"cookbook/aws-secrets-sync",
"cookbook/gpg-commit-signing",
"cookbook/per-conversation-secrets",
"cookbook/service-account-github-pat"
Expand Down
Loading