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
2 changes: 1 addition & 1 deletion contrib/chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: centaur
description: Helm chart for the trusted Centaur control plane
type: application
version: 0.1.103
version: 0.1.104
appVersion: "0.1.0"
dependencies:
- name: connect
Expand Down
12 changes: 2 additions & 10 deletions contrib/chart/templates/apirs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{{- $mcpPublicUrl := default .Values.slackbotv2.mcpPublicUrl .Values.apiRs.mcpPublicUrl -}}
{{- $apiRsName := include "centaur.componentName" (dict "root" . "component" "api-rs") -}}
{{- $sandboxOverlayMountPath := .Values.overlay.sandboxMountPath | default "/home/agent/overlay/org" -}}
{{- $githubAppBrokerBootstrapEnabled := and (or .Values.tokenBroker.enabled .Values.tokenBroker.githubApp.enabled) .Values.tokenBroker.githubApp.enabled .Values.tokenBroker.githubApp.bootstrap.enabled -}}
{{- $githubAppBrokerBootstrapEnabled := and .Values.tokenBroker.githubApp.enabled .Values.tokenBroker.githubApp.bootstrap.enabled -}}
{{- if and $githubAppBrokerBootstrapEnabled (not $console.enabled) -}}
{{- fail "console.enabled must be true when tokenBroker.githubApp bootstrap is enabled" -}}
{{- end -}}
Expand All @@ -13,14 +13,6 @@
{{- $githubAppBrokerBootstrapImageRepository := default .Values.sandbox.image.repository .Values.tokenBroker.githubApp.bootstrap.image.repository -}}
{{- $githubAppBrokerBootstrapImageTag := default .Values.sandbox.image.tag .Values.tokenBroker.githubApp.bootstrap.image.tag -}}
{{- $githubAppBrokerBootstrapImagePullPolicy := default .Values.sandbox.image.pullPolicy .Values.tokenBroker.githubApp.bootstrap.image.pullPolicy -}}
{{- $githubAppBrokerCredentialIds := list "github-app" -}}
{{- with .Values.tokenBroker.githubApp.credentialId -}}
{{- $githubAppBrokerCredentialIds = append $githubAppBrokerCredentialIds . -}}
{{- end -}}
{{- range .Values.tokenBroker.githubApp.extraCredentialIds -}}
{{- $githubAppBrokerCredentialIds = append $githubAppBrokerCredentialIds . -}}
{{- end -}}
{{- $githubAppBrokerCredentialIds = uniq $githubAppBrokerCredentialIds -}}
{{- $githubAppBrokerNamespaces := default (list "default") .Values.tokenBroker.githubApp.credentialNamespaces -}}
{{- $repoCacheStorageType := include "centaur.repoCacheStorageType" . -}}
{{- $repoCacheUsePvc := eq $repoCacheStorageType "persistentVolumeClaim" -}}
Expand Down Expand Up @@ -360,7 +352,7 @@ spec:
name: {{ .Values.tokenBroker.githubApp.existingSecretName | quote }}
key: {{ .Values.tokenBroker.githubApp.existingSecretKeys.installationId | quote }}
- name: GITHUB_APP_BROKER_CREDENTIAL_IDS
value: {{ join " " $githubAppBrokerCredentialIds | quote }}
value: "github-app"
- name: GITHUB_APP_BROKER_NAMESPACES
value: {{ join " " $githubAppBrokerNamespaces | quote }}
volumeMounts:
Expand Down
12 changes: 1 addition & 11 deletions contrib/chart/tests/test_overlay_image_compat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,6 @@ for expected in \
fi
done

helm template test "$chart_dir" "${common[@]}" \
--set console.enabled=true \
--set tokenBroker.githubApp.enabled=true \
--set tokenBroker.githubApp.credentialId=fineas-github-app \
--set tokenBroker.githubApp.existingSecretName=fineas-github-app \
>"$scratch/github-app-alias.yaml"
if ! grep -qF 'value: "github-app fineas-github-app"' "$scratch/github-app-alias.yaml"; then
echo "GitHub App compatibility alias must not replace the canonical github-app credential" >&2
exit 1
fi

helm template test "$chart_dir" "${common[@]}" \
--set console.enabled=true \
--set tokenBroker.githubApp.enabled=true \
Expand All @@ -205,6 +194,7 @@ for expected in \
'name: github-app-broker-bootstrap' \
'grant: "github_app_installation"' \
'name: GITHUB_APP_ID' \
'value: "github-app"' \
'name: github-app-broker-secret' \
'secretName: "fineas-github-app"'; do
if ! grep -qF "$expected" "$scratch/github-app.yaml"; then
Expand Down
6 changes: 0 additions & 6 deletions contrib/chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,10 @@
"tokenBroker": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"githubApp": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"credentialId": { "type": "string" },
"extraCredentialIds": {
"type": "array",
"items": { "type": "string" }
},
"credentialNamespaces": {
"type": "array",
"items": { "type": "string" }
Expand Down
12 changes: 3 additions & 9 deletions contrib/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,18 +190,12 @@ console:
replicaCount: 1
resources: {}

# Managed short-lived token credentials for sandbox proxy rules. This
# transitional bootstrap upserts the GitHub App installation credential into
# the upstream Console credential-grant registry before api-rs starts.
# Managed short-lived token credentials for sandbox proxy rules. The bootstrap
# upserts the GitHub App installation credential into the upstream Console
# credential-grant registry before api-rs starts.
tokenBroker:
enabled: false
githubApp:
enabled: false
# Compatibility alias provisioned alongside the canonical `github-app`
# credential. The built-in infra role always consumes `github-app`; do not
# add a second same-scope proxy source for this alias.
credentialId: github-app
extraCredentialIds: []
credentialNamespaces:
- default
- infra
Expand Down
3 changes: 0 additions & 3 deletions docs/pages/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ These must exist for the normal Helm deployment. For local development,
| `SLACK_BOT_TOKEN` | `secretManager.existingSecretName`; local bootstrap reads shell env. | Slack Web API access for Slackbot and api-rs Slack helpers. |
| `SANDBOX_SIGNING_KEY` | `secretManager.existingSecretName`; local bootstrap generates it. | Signing key for short-lived sandbox API tokens. |
| `IRON_MANAGEMENT_API_KEY` | `secretManager.existingSecretName`; local bootstrap generates it. | Management key for API-created iron-proxy pods. |
| `IRON_BROKER_TOKEN` | `secretManager.existingSecretName`; required when `tokenBroker.enabled=true`. | Bearer token iron-proxy presents to iron-token-broker and the broker enforces on its HTTP API. |
| `OP_SERVICE_ACCOUNT_TOKEN` | Local shell, then `centaur-infra-env`; production Secret. | 1Password service-account auth when using `onepassword` secret source. |
| `OP_VAULT` | Local shell, then `centaur-infra-env`; defaults to `ai-agents` in code. | 1Password vault used for `op://...` secret refs. |

Expand Down Expand Up @@ -192,8 +191,6 @@ Kubernetes backend:
| `KUBERNETES_SECRET_ENV_NAME`, `KUBERNETES_SECRET_ENV_PREFIX`, `KUBERNETES_BOOTSTRAP_SECRET_NAME` | `secretManager.*`, `secrets.bootstrapSecretName`. | Secrets read by API-created proxy/sandbox pods. |
| `KUBERNETES_IRON_PROXY_IMAGE`, `KUBERNETES_IRON_PROXY_IMAGE_PULL_POLICY`, `KUBERNETES_IRON_PROXY_PORT`, `KUBERNETES_IRON_PROXY_MANAGEMENT_PORT`, `KUBERNETES_IRON_PROXY_HEALTH_PORT` | `ironProxy.*`. | Per-sandbox iron-proxy image and ports. |
| `FIREWALL_MANAGER_SECRET_SOURCE`, `FIREWALL_MANAGER_SECRET_TTL`, `KUBERNETES_FIREWALL_MANAGER_SECRET_SOURCE` | `ironProxy.secretSource`, `ironProxy.secretTtl`. | Secret source and cache TTL for rendered proxy config. |
| `FIREWALL_MANAGER_TOKEN_BROKER_TTL` | `tokenBroker.ttl`. | Proxy-side cache TTL for access tokens minted by iron-token-broker. Applied to every `brokered_token` secret. |
| `KUBERNETES_TOKEN_BROKER_NAME`, `KUBERNETES_TOKEN_BROKER_URL` | `tokenBroker.*`. | iron-token-broker Deployment name and ClusterIP URL. The chart owns the broker Deployment, Service, and NetworkPolicies; the API reconciles its ConfigMap and triggers a rolling restart when the rendered content changes. |
| `KUBERNETES_OP_CONNECT_HOST`, `KUBERNETES_OP_CONNECT_APP_NAME`, `KUBERNETES_OP_CONNECT_PORT` | Chart helper or `api.extraEnv`. | 1Password Connect endpoint details. |
| `KUBERNETES_API_POD_LABEL_SELECTOR` | Chart-rendered labels or `api.extraEnv`. | API pod selector for API-managed proxy policies. |
| `KUBERNETES_EGRESS_DISCOVERY_ENABLED`, `KUBERNETES_EGRESS_SERVICE_NAMESPACE`, `KUBERNETES_CLUSTER_DOMAIN`, `KUBERNETES_EGRESS_TAILNET_FQDN_ANNOTATION` | `api.egressDiscovery.*`. | Egress service discovery for sandbox NetworkPolicies. |
Expand Down
3 changes: 0 additions & 3 deletions docs/public/md/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ These must exist for the normal Helm deployment. For local development,
| `SLACK_BOT_TOKEN` | `secretManager.existingSecretName`; local bootstrap reads shell env. | Slack Web API access for Slackbot and api-rs Slack helpers. |
| `SANDBOX_SIGNING_KEY` | `secretManager.existingSecretName`; local bootstrap generates it. | Signing key for short-lived sandbox API tokens. |
| `IRON_MANAGEMENT_API_KEY` | `secretManager.existingSecretName`; local bootstrap generates it. | Management key for API-created iron-proxy pods. |
| `IRON_BROKER_TOKEN` | `secretManager.existingSecretName`; required when `tokenBroker.enabled=true`. | Bearer token iron-proxy presents to iron-token-broker and the broker enforces on its HTTP API. |
| `OP_SERVICE_ACCOUNT_TOKEN` | Local shell, then `centaur-infra-env`; production Secret. | 1Password service-account auth when using `onepassword` secret source. |
| `OP_VAULT` | Local shell, then `centaur-infra-env`; defaults to `ai-agents` in code. | 1Password vault used for `op://...` secret refs. |

Expand Down Expand Up @@ -192,8 +191,6 @@ Kubernetes backend:
| `KUBERNETES_SECRET_ENV_NAME`, `KUBERNETES_SECRET_ENV_PREFIX`, `KUBERNETES_BOOTSTRAP_SECRET_NAME` | `secretManager.*`, `secrets.bootstrapSecretName`. | Secrets read by API-created proxy/sandbox pods. |
| `KUBERNETES_IRON_PROXY_IMAGE`, `KUBERNETES_IRON_PROXY_IMAGE_PULL_POLICY`, `KUBERNETES_IRON_PROXY_PORT`, `KUBERNETES_IRON_PROXY_MANAGEMENT_PORT`, `KUBERNETES_IRON_PROXY_HEALTH_PORT` | `ironProxy.*`. | Per-sandbox iron-proxy image and ports. |
| `FIREWALL_MANAGER_SECRET_SOURCE`, `FIREWALL_MANAGER_SECRET_TTL`, `KUBERNETES_FIREWALL_MANAGER_SECRET_SOURCE` | `ironProxy.secretSource`, `ironProxy.secretTtl`. | Secret source and cache TTL for rendered proxy config. |
| `FIREWALL_MANAGER_TOKEN_BROKER_TTL` | `tokenBroker.ttl`. | Proxy-side cache TTL for access tokens minted by iron-token-broker. Applied to every `brokered_token` secret. |
| `KUBERNETES_TOKEN_BROKER_NAME`, `KUBERNETES_TOKEN_BROKER_URL` | `tokenBroker.*`. | iron-token-broker Deployment name and ClusterIP URL. The chart owns the broker Deployment, Service, and NetworkPolicies; the API reconciles its ConfigMap and triggers a rolling restart when the rendered content changes. |
| `KUBERNETES_OP_CONNECT_HOST`, `KUBERNETES_OP_CONNECT_APP_NAME`, `KUBERNETES_OP_CONNECT_PORT` | Chart helper or `api.extraEnv`. | 1Password Connect endpoint details. |
| `KUBERNETES_API_POD_LABEL_SELECTOR` | Chart-rendered labels or `api.extraEnv`. | API pod selector for API-managed proxy policies. |
| `KUBERNETES_EGRESS_DISCOVERY_ENABLED`, `KUBERNETES_EGRESS_SERVICE_NAMESPACE`, `KUBERNETES_CLUSTER_DOMAIN`, `KUBERNETES_EGRESS_TAILNET_FQDN_ANNOTATION` | `api.egressDiscovery.*`. | Egress service discovery for sandbox NetworkPolicies. |
Expand Down
47 changes: 36 additions & 11 deletions services/api-rs/crates/centaur-api-server/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2630,6 +2630,15 @@ fn authorize_workflow_api(headers: &HeaderMap) -> Result<WorkflowApiAuthorizatio
}

fn authorize_session_api(headers: &HeaderMap) -> Result<WorkflowApiAuthorization, ApiError> {
authorize_session_api_with_service_token_matcher(headers, |token| {
token_matches_configured_env(token, SESSION_API_SERVICE_KEY_ENVS)
})
}

fn authorize_session_api_with_service_token_matcher(
headers: &HeaderMap,
service_token_matches: impl Fn(&str) -> bool,
) -> Result<WorkflowApiAuthorization, ApiError> {
if let Some(presented) = header_value(headers, "X-Centaur-Feedback-Key") {
if !token_matches_configured_env(presented.trim(), &["SLACK_FEEDBACK_API_KEY"]) {
return Err(ApiError::Unauthorized(
Expand All @@ -2645,18 +2654,8 @@ fn authorize_session_api(headers: &HeaderMap) -> Result<WorkflowApiAuthorization
return Ok(WorkflowApiAuthorization::FeedbackImprovement(claims));
}

let slackbot_key = env::var("SLACKBOT_API_KEY").unwrap_or_default();
let slackbot_key = slackbot_key.trim();
if !slackbot_key.is_empty()
&& header_value(headers, "X-Api-Key").is_some_and(|presented| {
constant_time_eq(presented.trim().as_bytes(), slackbot_key.as_bytes())
})
{
return Ok(WorkflowApiAuthorization::Service);
}

let token = bearer_token(headers)?;
if token_matches_configured_env(token, SESSION_API_SERVICE_KEY_ENVS) {
if service_token_matches(token) {
return Ok(WorkflowApiAuthorization::Service);
}
let claims: WorkflowApiClaims = verify_console_jwt(token)?;
Expand Down Expand Up @@ -4070,6 +4069,32 @@ mod workflow_api_tests {
assert!(!constant_time_eq(b"service-token", b"other-token"));
}

#[test]
fn session_api_uses_the_bearer_lane_for_slackbot_credentials() {
assert!(SESSION_API_SERVICE_KEY_ENVS.contains(&"SLACKBOT_API_KEY"));

let mut bearer_headers = HeaderMap::new();
bearer_headers.insert(
axum::http::header::AUTHORIZATION,
"Bearer service-token".parse().unwrap(),
);
assert!(matches!(
authorize_session_api_with_service_token_matcher(&bearer_headers, |token| {
token == "service-token"
}),
Ok(WorkflowApiAuthorization::Service)
));

let mut legacy_headers = HeaderMap::new();
legacy_headers.insert("X-Api-Key", "service-token".parse().unwrap());
assert!(matches!(
authorize_session_api_with_service_token_matcher(&legacy_headers, |token| {
token == "service-token"
}),
Err(ApiError::Unauthorized(_))
));
}

#[test]
fn workflow_principals_cannot_list_schedules_or_emit_global_events() {
for operation in ["schedules", "events"] {
Expand Down
2 changes: 0 additions & 2 deletions services/console/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,6 @@ The token credentials it refreshes with are fields on the credential, resolved b

GitHub traffic is wired to this broker by the built-in sandbox proxy fragment (`centaur-iron-proxy/src/infra.yaml`): `github.com` and `api.github.com` already carry a `token_broker` source referencing the `github-app` credential, so a deployment only needs to provision that credential — no per-deployment static secret. The Helm chart can provision it at api-rs startup when `tokenBroker.githubApp.enabled=true` and `tokenBroker.githubApp.existingSecretName` points at a Kubernetes Secret containing the App ID, installation ID, and private key. The fragment uses a `replace` (placeholder swap) rather than a header `inject`, which preserves the caller's auth scheme: `git` over HTTPS keeps its Basic `x-access-token:<token>` form (`github.com` rejects `Bearer` for git transport) while the REST API keeps `Bearer`. To provision it manually instead of via Helm, run:

`tokenBroker.githubApp.credentialId` and `extraCredentialIds` are compatibility aliases provisioned alongside the canonical `github-app` credential. They are bootstrap-only unless a separate, explicitly scoped secret references them; the built-in GitHub rules intentionally have one source so equal-priority credentials cannot compete for the same Authorization header.

```
centaur-perms broker create --namespace default --foreign-id github-app \
--grant github_app_installation --client-id "$GITHUB_APP_ID" \
Expand Down
Loading