Skip to content

Add JSON-field token extraction and custom admin headers to ServiceDeployer - #105

Merged
pcholakov merged 4 commits into
restatedev:mainfrom
capitalone-contributions:service-deployer-auth-options
Sep 23, 2026
Merged

pcholakov merged 4 commits into
restatedev:mainfrom
capitalone-contributions:service-deployer-auth-options

Conversation

@nolanmurphey

Copy link
Copy Markdown
Contributor

Implements tier 1 of #103 — declarative authTokenJsonField and additionalHeaders.

Adds two opt-in registration options to the Restate ServiceDeployer, for environments where the admin auth token isn't a bare string or the admin endpoint sits behind a proxy.

Options

  • authTokenJsonField — when the auth-token secret holds a JSON object (e.g. {"token":"rst_xxx","version":3}), extract the bearer token from the named top-level field instead of using the whole secret string. Extraction happens at runtime inside the custom-resource handler (a JSON.parse after the existing GetSecretValue), not via secretValueFromJson() at synth, so the plaintext token never resolves into a CloudFormation property or CloudWatch.
  • additionalHeaders — static headers added to every admin API request (health check, registration, visibility patch, pruning, and deletion). Spread last, so a caller can override the handler-set Authorization, Content-Type, and Accept when a proxy or gateway requires it.

Both are only forwarded to the custom resource when set, so existing stacks see no CloudFormation property diff. additionalHeaders is also preserved on the best-effort delete path when the token fails to load, since those headers don't depend on the secret.

Changes

  • service-deployer.ts — new authTokenJsonField and additionalHeaders props on ServiceRegistrationProps, forwarded to the custom resource only when set.
  • register-service-handler/index.mts — createAuthHeader → buildBaseHeaders; spreads additionalHeaders last and does secret-safe JSON-field extraction that never echoes secret material on error.
  • Tests for forward-when-set and omit-when-unset; updated handler bundle snapshot.

Scope

Tier 1 only: both options ride across as custom-resource properties and are applied by the existing shipped handler, so the deployer Lambda is unchanged.

Tests

npm run build, npm run lint, and npm test all pass (14/14 tests, 9/9 snapshots).

…tration

Support extracting a bearer token from a JSON field of the auth-token
secret, and forwarding static headers on every admin API request. Both
options are opt-in and only forwarded to the custom resource when set,
avoiding CFN property diffs for existing users. JSON extraction happens
inside the handler so the plaintext token is never exposed as a
custom-resource property.

Co-Authored-By: Claude Code
Simplify header handling: instead of stripping reserved headers
(Authorization, Content-Type, Accept) from additionalHeaders, spread
additionalHeaders last so a caller can override them when a proxy or
gateway in front of the admin endpoint requires it.

Co-Authored-By: Claude Code
buildBaseHeaders returns both the bearer token and the caller's static
headers, so the delete path's catch was discarding headers that never
depended on the secret. A caller using additionalHeaders to satisfy a
proxy in front of the admin endpoint would see the best-effort delete
rejected, silently leaving the deployment registered in Restate.

Co-Authored-By: Claude Code
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@nolanmurphey
nolanmurphey marked this pull request as ready for review September 22, 2026 20:29
@nolanmurphey

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@pcholakov
pcholakov self-requested a review September 23, 2026 12:30

@pcholakov pcholakov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hey @nolanmurphey, this is a solid take on tier 1 of #103; only forwarding the new props when set means existing stacks see no property diff. I am approving - leaving some non-blocking notes below, but we're happy to pick any of them up in a follow-up before the next release if you'd rather not do this right now.

additionalHeaders values are stored and logged in plaintext. They're custom-resource properties so they'll show up in the synthesized template; also the handler logs the whole event on entry (console.log({ event })), which would dump them in CloudWatch Logs. Two cheap mitigations: a line in the prop docs saying not to put credentials there, and logging only header names in that event log. We can do the log redaction on our side. A secret-backed header option could get added in tier 2.

Overriding Accept breaks cleanup silently. The /query call relies on Accept: application/json, because Restate otherwise returns Arrow IPC data. This can break pruning and delete-time cleanup, and since both only log a warning, this risks a deployment silently staying registered after resource deletion. Authorization is a reasonable header to override with the secret storage caveat. For Accept and Content-Type, either add a line to the docs - or keep them fixed.

Handler tests. The new tests cover forwarding at synth time. The handler logic itself isn't covered: JSON extraction and its error cases, header precedence, and the delete-path fallback from the third commit. Exporting buildBaseHeaders, or pulling the extraction out as a pure function, would make these easy to test with a mocked Secrets Manager client.

* satisfying a proxy/gateway in front of the Restate admin endpoint.
*
* These are applied by the shipped handler and do not require bundling. They take precedence over headers the
* handler sets itself (`Authorization`, `Content-Type`, `Accept`); use standard header casing to override one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can drop "use standard header casing to override" here and in the handler's RegistrationProperties doc. Node lowercases header names and the last one set wins, so authorization overrides Authorization as well (confirmed against a local server).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah you're right - updated!

adminUrl: options?.adminUrl ?? environment.adminUrl,
authTokenSecretArn: authToken?.secretArn,
// Forward JSON-field extraction and extra headers only when set, to avoid CFN property diffs for existing users.
...(options?.authTokenJsonField !== undefined ? { authTokenJsonField: options.authTokenJsonField } : {}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

authTokenJsonField without an auth token is silently ignored. A synth-time error would catch the misconfiguration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch - added a synth-time check

* `{"token":"rst_xxx","version":3}`, set this to `"token"`. Only flat, top-level keys are supported; nested paths
* are not.
*/
authTokenJsonField?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The JSON shape belongs to the secret, so I think this might work better in RestateEnvironment next to authToken rather than being repeated on every register() call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right that would make more sense. Moved this to RestateEnvironment and added the caveat that it wouldn't be used if you override at the per-registration level!

throw new Error(`Secret value is not valid JSON; cannot extract field "${props.authTokenJsonField}".`);
}
const field = parsed?.[props.authTokenJsonField];
if (typeof field !== "string") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the JSON field holds an empty string, it passes this check and will send Bearer with no token. Worth rejecting the same way as a missing field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a check for this!

Narrow auth review follow-up

Move JSON token field to RestateEnvironment

Test JSON auth token extraction

Revert JSON extraction unit test refactor
@pcholakov
pcholakov merged commit 5df90e6 into restatedev:main Sep 23, 2026
2 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants