> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/howto/upstream-sso-login · Doc index: https://docs.gatellm.io/en/llms.txt


# Upstream SSO credential login

Some upstreams (such as Kiro) do not accept a static API key but require you to log in via OAuth to obtain an **expiring refresh credential**. When the gateway forwards requests, it must use that credential to exchange for a temporary access token and put it into the request headers sent to the upstream. This page covers how the admin **logs in and imports that credential into the gateway in one go**.

No need to create the upstream by hand: **the login itself is the entire operation** — the first login auto-creates the vendor's corresponding upstream, and subsequent logins append new accounts' credentials under the same upstream. After logging in, go to Console → **Upstreams** → that SSO upstream (with the SSO badge) → the credentials section to view the status of each account's credential, or copy that `docker run … login …` command to keep adding accounts.

> Core mechanism: **login happens on the admin's local machine, not in the gateway.** The gateway is a remote service and cannot receive the browser's loopback redirect; so the gateway image bundles a `login` subcommand that you run on your own machine — it starts a temporary listener locally to catch the browser callback, completes the OAuth authorization, obtains the refresh credential, then calls the gateway's import API with the console access key to report the credential, and immediately exits, leaving no credential on disk. The gateway only ever receives one already-minted credential, minimizing the exposure surface.

> **Don't confuse this with [Configure SSO enterprise login](/en/howto/configure-sso.md)**: that page covers "authenticating **who calls the gateway** with an enterprise IdP" (data-plane token / console enterprise login); this page covers "the admin logging into an **upstream vendor** (e.g. Kiro) to obtain a refresh credential for the gateway to forward with". The directions are opposite: one authenticates traffic into the gateway, the other gives the gateway something to use against the upstream.

## Step 1: configure the console access key first

This is a **prerequisite**; without it the whole flow gets stuck at 401. The `login` subcommand authenticates to the gateway using the **console access key** (`web_console.secret_key`), not the data-plane client access key.

The gateway's `secret_key` in the `[web_console]` section is a long-lived static key (bcrypt-hashed storage) that, when matched, injects **full admin permissions**. Configuration requirements:

- Non-empty and ≥ 12 characters, otherwise configuration validation errors directly;
- Not configured by default (`None`), **not auto-generated**, must be set manually;
- In database-storage mode, write `secret_key = "your key"` in the `[web_console]` section of `server.toml`; the Docker image injects it via the environment variable `CONSOLE_SECRET_KEY`.

> ⚠️ This key = full admin permissions. Pass it via an environment variable or a `:ro` (read-only) file mount, and delete it after use; don't put it in command-line arguments, version control, or chat logs.

## Step 2: confirm the container can reach the gateway console

The `login` container must send the import request to the gateway console. Remote access to the console is controlled by `allow_remote`; note the **two layers have different defaults**: **the config file defaults to `false` (only allows local loopback)**, while **the official Docker image allows remote by default via `CONSOLE_ALLOW_REMOTE=true`** (see [Environment variable reference](/en/reference/configuration.md) and [Console login & roles → Security](/en/console/login-and-roles.md#security)). If your gateway is running with the default `false` (e.g. bare metal / source run), the container's source IP coming through the bridge network is not loopback and will be blocked as 403. **Pick one of the two paths**:

1. **`docker run --network host`** (recommended, default config untouched): the container shares the host network, so the source IP is loopback and the default config already allows it.
2. **`allow_remote = true`** (change the gateway config): open the console's remote access. Note this enlarges the console's exposure surface, and with a reverse proxy on the same host the source IP is `127.0.0.1` and the existing check cannot block a remote client coming through the proxy — pre-existing semantics, not introduced here.

> The check is based on the TCP peer address, not `X-Forwarded-For`, so `--network host` is the least-fussy option.

## Step 3: run the login command

Run the command below on a machine that can open a browser. The access key is passed via an **environment variable**; `login` deliberately provides no `--access-key <plaintext>` command-line-argument form — command-line arguments go into the `ps` process list, shell history, and `docker inspect`, and secrets must not land in the command line. `--entrypoint protoflux` must be written explicitly, otherwise Docker wrappers like lima/colima will parse the bare `login` as a host command and execute the system's `/usr/bin/login` (erroring `Cannot possibly work without effective root`):

```bash
docker run --rm -it --network host \
  --entrypoint protoflux \
  -e PROTOFLUX_ACCESS_KEY='<the secret_key value>' \
  <gateway image> login \
    --vendor kiro \
    --gateway http://localhost:7890
```

> Replace `<gateway image>` with **the same image reference you deploy the gateway with** — the same image used in step 1's `docker pull` / `docker run` of [Docker single node](/en/quickstart/docker-single-node.md) (an official release looks like `ghcr.io/<org>/<image>:<tag>`). The `login` subcommand is built into this image, no separate install needed.

Parameter description:

| Parameter | Meaning |
|---|---|
| `-e PROTOFLUX_ACCESS_KEY=<value>` | The `secret_key` configured in step 1, passed via environment variable (recommended) |
| `--vendor kiro` | The upstream SSO vendor id to log in, matching the `sso_vendor` selected in the upstream form |
| `--gateway http://localhost:7890` | The gateway console address. Under `--network host` use `localhost`; otherwise use the host IP or `host.docker.internal`. **Must be https, or http + loopback/private address** — plaintext http over the public internet is always rejected |
| `--account <account>` (optional) | The account identifier for this credential (e.g. the login email). Re-login with the same name **replaces** the old credential, a different name **appends** a new account. Default auto-takes the vendor-returned account identity; if the vendor gives none it lands in the "unnamed account" slot |
| `--upstream-id 7` (optional) | The stable id of the target upstream (the numeric id visible in the console upstream list). **Usually not needed**: by default the gateway auto-resolves or creates that vendor's upstream |
| `--port 3128` (optional) | The local callback listen port. Default auto-picks the first idle port in the vendor IdP's allowlist (kiro's first allowlist entry is 3128 — the IdP only recognizes the fixed port set registered for the official IDE, and a temporary port is rejected). When publishing the port with `docker -p 127.0.0.1:3128:3128`, you must pin the same allowlist port and keep it consistent; under `--network host` it can be omitted |
| `--access-key-file <path in container>` (optional) | An alternative to the environment variable: store the key as a local file, mount it read-only with `-v /path/to/key.txt:/key:ro`, then point to it with this parameter (suits file-based key management like docker secret) |

The key value is taken in three tiers (in this order): the `--access-key-file` file → the `PROTOFLUX_ACCESS_KEY` environment variable → if neither, a no-echo interactive prompt. All three tiers avoid command-line arguments.

After the command starts, this happens: it starts a temporary listener on a local loopback allowlist port (kiro's default first idle 3128) → opens a browser to jump to the IdP login → you complete authorization in the browser → the IdP redirects the authorization code back to the local listener → the subcommand exchanges for the refresh credential using PKCE (Proof Key for Code Exchange) → calls the gateway import API to report the credential → exits. The whole time the **refresh credential never lands on your machine and never enters gateway logs**.

> A browser usually can't be launched from inside the container; the command prints the authorization URL — copy and open it in the host browser.

## Enterprise login: IAM Identity Center (AWS IDC)

The PKCE flow above covers social logins in the Kiro portal such as **Builder ID, Google, GitHub**. If your enterprise uses **AWS IAM Identity Center** (formerly AWS SSO) to manage Kiro access, login uses the **same `login` command and the same portal entry**, differing only in which login method you pick in the browser — the command itself needs no extra parameters, and the CLI auto-routes based on the portal callback's shape.

Steps:

1. Run `login --vendor kiro …` as usual; the browser opens the Kiro portal login page;
2. On the login page pick **IAM Identity Center** (not Builder ID / Google / GitHub);
3. Follow the prompt to enter the **enterprise start URL** your admin gave you (an AWS IDC tenant URL like `https://d-example12345.awsapps.com/start`, the exact value provided by your enterprise admin);
4. The portal redirects back to the local listener — this time the callback carries **no authorization code** but rather `login_option=awsidc` + the enterprise start URL + the IDC region. After the CLI recognizes this intermediate callback, it returns a "completing device authorization" page to the browser, then prints a **device verification link** in the terminal;
5. Open that verification link (or the URL printed in the terminal) and complete device authorization in the browser;
6. After authorization, the CLI obtains the access token + refresh token and reports to the gateway as usual.

Technical difference (admins usually don't need to care; for troubleshooting only): social-login credential refresh hits Kiro's desktop endpoint; IDC credential refresh hits AWS SSO-OIDC's `oidc.{region}.amazonaws.com/token`, and each enterprise's IDC region differs — the CLI stores the enterprise start URL and IDC region together with the credential in the gateway and reads them per-credential on renewal, so you don't configure them. An imported IDC credential row shows `kiro_idc` in the `Source` column, and a social login shows `kiro_desktop`; the two kinds of credentials can coexist under the same Kiro upstream and the gateway auto-rotates them.

> **IDC credentials need re-login every 90 days**: the server background task auto-renews the access token (scanning every 60 seconds, refreshing 5 minutes early), and what it renews is the access token, not the client secret. The OIDC client secret (`client_id`/`client_secret`) registered by the device flow is valid for about 90 days, after which refresh starts to fail — at that point refresh cannot self-recover (retries still use the same expired key pair), the credential turns terminal and is marked "client secret expired, re-login required". Then re-run the login command above once (the same account replaces the old credential) — no need to delete the old row. Social-login credentials involve no client secret and have no such limit.

## Multiple accounts: each additional login adds one account

A vendor (e.g. kiro) **always has exactly one upstream** in the gateway, under which multiple accounts' credentials hang — the same logic as "one upstream with multiple API keys", and the gateway auto-rotates requests across the accounts. **Adding one more account = run the login command above one more time** (authorize with another account):

- The new account's credential appends under the same upstream and immediately joins rotation, no restart or config change;
- To distinguish rows clearly, add `--account email-or-alias`; default auto-takes the vendor-returned account identity;
- Re-logging in the same account (`--account` the same, or the vendor-returned identity the same) **replaces** that account's old credential — re-login once to fix it when the credential expires.

## Managing account credentials (view / delete)

Console → **Upstreams** → open that SSO upstream → the credentials section lists **one row per account**: account identifier, status (active / expired / error), source, expiration time.

- **Delete an account**: click the delete icon on the right of that row, then confirm "delete". After deletion that account immediately stops receiving traffic and the gateway stops renewing it; to restore, re-login and import.
- Deletion only affects that one account row; other accounts under the same upstream are unaffected.

## Credentials are auto-renewed by the gateway

After import, credentials need no maintenance: the gateway's background task **actively renews them before expiry** (scanning every 60 seconds by default, refreshing 5 minutes early), so even a long-dormant upstream won't silently expire its credentials. When a renewal occasionally fails the gateway auto-backs-off and retries; a row with status `error` self-heals with no manual intervention — the only case needing a human is when the upstream revokes the credential (status revoked, usually from long non-renewal or an explicit logout on the upstream side), at which point re-run the login command once to import.

Multi-instance deployment also needs no configuration: instances auto-mutual-exclude (only one instance renews each credential at any moment), and renewal results auto-propagate to the rest. See [Environment variable reference → Config-file-only fields](/en/reference/configuration.md#config-file-only-fields) for the related parameters (`sso_credentials.*`); the defaults work.

## Fallback: manual import

When login fails (IdP endpoint behavior changes, network issues, etc.), you can do a **manual import**: complete login in Kiro's official UI/client, copy the produced refresh credential, paste it into the "manual paste" box in the console upstream's credentials section and click import (you can optionally give an account identifier for multi-account dedup). Zero scripts, zero login-endpoint dependency — a universal fallback for any vendor.

## Troubleshooting

- **401**: `secret_key` is not configured (`web_console.secret_key` empty, console API not enabled) or the value doesn't match. The backend cannot distinguish the two, and the error text lists both possibilities.
- **403**: console remote access denied. Use `--network host`, or set `allow_remote = true` in the gateway config.
- **Key just changed but the old value still works**: `verify_bearer` has a 300-second positive cache (keyed by token hash); after rotation the old key may still be valid for up to 300 seconds; it auto-expires after the timeout.
- **Banned after repeated failed attempts**: the console has failure counting and IP blocking. Repeated trial-and-error with the wrong key triggers it; wait out the ban duration or switch IPs.
- **Callback not received**: confirm `--port` matches the `docker -p` mapping and the port isn't occupied; under `--network host` no `-p` is needed.
- **Browser reports redirect_mismatch**: the callback port isn't in the vendor IdP's allowlist. Default behavior already picks an allowlist port; if you manually specified `--port`, change it to one inside the allowlist (kiro: 3128, 4649, 6588, 8008, 9091, etc.).
- **`Cannot possibly work without effective root`**: the command didn't write `--entrypoint protoflux`; Docker wrappers like lima/colima parsed the bare `login` as the system command `/usr/bin/login` (rejected without privilege). Add `--entrypoint protoflux`.

## Operational notes

- For import operations reported via `secret_key`, the audit log uniformly records the actor as `secret_key` and cannot distinguish which tool / which login initiated it.
- This key grants full admin permissions on match and is a long-lived high-privilege credential: mount `:ro`, delete after use; rotate by changing config + reloading (note the 300-second cache window).
- The imported refresh credential is encrypted with the master key `ENCRYPTION_KEY` and stored in the database; the gateway **refuses to work** without it (fail-closed, not plaintext degradation). If not configured, first generate one with `openssl rand -hex 32` and restart with it, see [Environment variable reference → ENCRYPTION_KEY](/en/reference/configuration.md#storage-multi-instance) — **be sure to back it up; losing it makes already-encrypted credentials permanently unreadable**.

**Next**: [Configure SSO enterprise login](/en/howto/configure-sso.md) for the reverse direction — authenticating who calls the gateway with an enterprise IdP; [Upstream & model fields](/en/reference/upstreams-models-fields.md) for the full upstream fields; [Endpoints · auth · protocol interop](/en/reference/endpoints.md) for the full auth-order picture.
