https://github.com/samn/gcp-authcalator
https://github.com/samn/gcp-authcalator
Last synced: 4 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/samn/gcp-authcalator
- Owner: samn
- License: mit
- Created: 2026-02-10T20:05:25.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-02-18T00:02:36.000Z (4 months ago)
- Last Synced: 2026-02-18T01:27:31.236Z (4 months ago)
- Language: TypeScript
- Size: 209 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# gcp-authcalator
A GCP auth escalator for containerized development environments.
Keeps GCP credentials out of devcontainers and AI coding agents by proxying token requests through a host-side daemon with confirmation dialogs for production access.
## Why
Modern IDEs encourage running AI coding agents in the same devcontainer the engineer works in. This is convenient — but it means every process inside the container, including unattended agents, has the same GCP credentials as the engineer. A single compromised dependency, a prompt-injection attack, or a malicious tool can silently use those credentials to write to production databases, decrypt secrets, or exfiltrate data.
The core problem is that `google.auth.default()` returns the engineer's full-privilege credentials to **any** process. There is no privilege boundary between the engineer's interactive session and automated tooling.
gcp-authcalator solves this by keeping credentials on the host and making the container ask for them:
1. A **token daemon** (`gate`) runs on the host and holds the engineer's Application Default Credentials. It mints short-lived, downscoped tokens via service account impersonation — never handing out the root credentials.
2. A **metadata server emulator** (`metadata-proxy`) runs inside the container, serving those downscoped tokens transparently to all Google Cloud client libraries. No application code changes needed.
3. **Production-level access requires explicit human confirmation** — a desktop dialog or terminal prompt on the host — so no automated process can silently escalate privileges.
Credentials never enter Docker. The Unix socket is the only channel, and the host daemon controls what tokens are issued.
## Architecture
```
┌─────────────────────────────────────────────┐
│ Host Machine │
│ │
│ ~/.config/gcloud/ ──▶ gcp-gate daemon │
│ (engineer creds) (Unix socket) │
│ ├─ confirmation UI │
│ └─ audit log │
└──────────────────┬──────────────────────────┘
│ $XDG_RUNTIME_DIR/gcp-authcalator.sock
┌──────────────────┴──────────────────────────┐
│ devcontainer │
│ │
│ gcp-metadata-proxy (127.0.0.1:8173) │
│ ▲ │
│ │ GCE_METADATA_HOST │
│ app / agent / tests │
│ │
│ with-prod ──▶ temp proxy (random port) │
│ ▲ │
│ │ GCE_METADATA_HOST │
│ elevated process │
└─────────────────────────────────────────────┘
```
## Prerequisites
Before using gcp-authcalator, set up GCP IAM:
1. **Create a service account** with limited permissions for development (e.g., `dev-runner@.iam.gserviceaccount.com`)
2. **Grant developers** the `roles/iam.serviceAccountTokenCreator` role on that service account
3. **Authenticate on the host** with `gcloud auth application-default login` so that Application Default Credentials (ADC) are available
The host-side `gate` daemon uses ADC to impersonate the service account via [`generateAccessToken`](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken), producing short-lived tokens (1-hour TTL).
## Installation
### From releases
Download a prebuilt binary from the [GitHub Releases](https://github.com/samn/gcp-authcalator/releases) page:
| Platform | Binary |
| ------------ | ------------------------------ |
| Linux x86_64 | `gcp-authcalator-linux-amd64` |
| macOS ARM64 | `gcp-authcalator-darwin-arm64` |
Each release includes SHA256 checksums for verification.
### From source
```bash
mise install
bun install
bun run build
```
This produces a single compiled `gcp-authcalator` binary.
## Configuration
Settings can be provided via CLI flags, a TOML config file, or both.
CLI flags take precedence over the config file, which takes precedence over defaults.
### CLI flags
```
--project-id GCP project ID
--service-account Service account email to impersonate
--socket-path Unix socket path (default: $XDG_RUNTIME_DIR/gcp-authcalator.sock)
-p, --port Metadata proxy port (default: 8173)
-c, --config Path to TOML config file
```
### TOML config file
```toml
project_id = "my-gcp-project"
service_account = "dev-runner@my-gcp-project.iam.gserviceaccount.com"
# socket_path defaults to $XDG_RUNTIME_DIR/gcp-authcalator.sock
# (or ~/.gcp-authcalator/gcp-authcalator.sock if XDG_RUNTIME_DIR is unset)
port = 8173
```
Pass the file with `--config`:
```bash
gcp-authcalator gate --config config.toml
```
## Commands
### `gate` — Host-side token daemon
Runs on the **host machine**. Listens on a Unix domain socket and mints GCP access tokens.
```bash
gcp-authcalator gate \
--project-id my-project \
--service-account dev-runner@my-project.iam.gserviceaccount.com
```
**Required options:** `--project-id`, `--service-account`
**API endpoints** (over Unix socket):
| Endpoint | Behavior |
| ----------------------- | ---------------------------------------------------------------- |
| `GET /token` | Returns a dev-scoped access token (impersonated service account) |
| `GET /token?level=prod` | Prompts for confirmation, then returns the engineer's own token |
| `GET /identity` | Returns the authenticated user's email |
| `GET /project-number` | Returns the numeric GCP project ID |
| `GET /universe-domain` | Returns the GCP universe domain |
| `GET /health` | Returns `{ "status": "ok", "uptime_seconds": N }` |
**Dev tokens** are minted by impersonating the configured service account. They are cached and re-minted when less than 5 minutes of lifetime remain.
**Prod tokens** use the engineer's own ADC credentials. Before issuing a prod token, the daemon:
1. Shows a desktop confirmation dialog (`osascript` on macOS, `zenity` on Linux)
2. Falls back to a terminal prompt if no GUI is available
3. Denies access if no interactive method is available
Prod token requests are rate-limited: one confirmation dialog at a time, a 5-second cooldown after denial, and a maximum of 5 attempts per minute.
**Audit logging:** All token requests are logged as JSON lines to the runtime directory's `audit.log` (`$XDG_RUNTIME_DIR/audit.log` or `~/.gcp-authcalator/audit.log`).
### `metadata-proxy` — Container-side metadata emulator
Runs **inside the devcontainer**. Emulates the [GCE metadata server](https://cloud.google.com/compute/docs/metadata/overview) so that all Google Cloud client libraries transparently fetch tokens from the proxy.
```bash
gcp-authcalator metadata-proxy --project-id my-project
```
**Required options:** `--project-id`
Set `GCE_METADATA_HOST=127.0.0.1:8173 GCE_METADATA_IP=127.0.0.1:8173` in the container environment so client libraries discover the proxy automatically.
**Endpoints:**
| Path | Response | `Metadata-Flavor: Google` required? |
| ----------------------------------------------------------------- | -------------------------------------- | ----------------------------------- |
| `GET /` | `200 ok` (detection ping) | No |
| `GET /computeMetadata/v1/instance/service-accounts/default/token` | Token JSON | Yes |
| `GET /computeMetadata/v1/project/project-id` | Plain text project ID | Yes |
| `GET /computeMetadata/v1/project/numeric-project-id` | Plain text numeric project ID | Yes |
| `GET /computeMetadata/v1/universe/universe_domain` | Plain text universe domain | Yes |
| `GET /computeMetadata/v1/instance/service-accounts/default/email` | Plain text SA email | Yes |
| `GET /computeMetadata/v1/instance/service-accounts/default` | SA info (JSON or directory listing) | Yes |
| `GET /computeMetadata/v1/instance/service-accounts` | SA listing (JSON or directory listing) | Yes |
Endpoints returning "JSON or directory listing" respond with JSON when `?recursive=true` is passed, and a text directory listing otherwise. This matches real GCE metadata server behavior.
Service account paths that use an email identifier (e.g., `.../service-accounts/sa@project.iam.gserviceaccount.com/token`) are automatically aliased to `default`, since the proxy serves a single set of credentials. This ensures compatibility with `gcloud` and other client libraries that resolve accounts by email.
The proxy fetches tokens from the `gate` daemon via the Unix socket and caches them locally, re-fetching when less than 5 minutes of lifetime remain.
### `with-prod` — Elevation wrapper
Wraps a shell command with production-level GCP credentials. Runs **inside the devcontainer**.
```bash
gcp-authcalator with-prod -- python some/script.py
gcp-authcalator with-prod -- gcloud sql instances list
gcp-authcalator with-prod -- alembic upgrade head
```
**Required options:** `--project-id`
This command:
1. Requests a prod token from `gate` (triggers a host-side confirmation dialog)
2. Starts a temporary metadata proxy on a random port serving that token
3. Creates an isolated `CLOUDSDK_CONFIG` directory so `gcloud` doesn't reuse cached credentials
4. Strips credential-related environment variables (`GOOGLE_APPLICATION_CREDENTIALS`, `CLOUDSDK_AUTH_ACCESS_TOKEN`, etc.) to force the child through the proxy
5. Spawns the wrapped command with `GCE_METADATA_HOST` and `GCE_METADATA_IP` pointing at the temporary proxy
6. Forwards signals to the child process and propagates its exit code
The temporary proxy uses PID-based process restriction — only the wrapped command and its descendants can request tokens from it.
### `kube-setup` — Patch kubeconfig for GKE
One-time setup command that patches your kubeconfig to use gcp-authcalator instead of `gke-gcloud-auth-plugin` for GKE cluster authentication.
```bash
gcp-authcalator kube-setup
```
This command:
1. Reads the kubeconfig (from `$KUBECONFIG` or `~/.kube/config`)
2. Finds all users with `exec.command: gke-gcloud-auth-plugin` (including full paths)
3. Replaces the exec section to point to `gcp-authcalator kube-token`
4. Creates a backup at `.bak`
5. Writes the patched kubeconfig back
After patching, kubeconfig user entries will look like:
```yaml
users:
- name: gke_project_region_cluster
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: /absolute/path/to/gcp-authcalator
args: ["kube-token"]
installHint: "Install gcp-authcalator or revert with: gcloud container clusters get-credentials "
provideClusterInfo: true
```
To revert, re-run `gcloud container clusters get-credentials `.
### `kube-token` — kubectl credential plugin
kubectl [exec credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins) that fetches a token from the active metadata proxy and outputs an `ExecCredential` JSON for kubectl. You don't call this directly — kubectl invokes it automatically after running `kube-setup`.
```bash
gcp-authcalator kube-token
```
The plugin reads `GCE_METADATA_HOST` from the environment (falls back to `127.0.0.1:8173`) and requests a token from that metadata proxy. This means it automatically picks up the correct token:
- **Normal usage:** fetches a dev token from the default metadata proxy
- **Under `with-prod`:** `GCE_METADATA_HOST` points to the temporary prod proxy, so kubectl transparently gets the prod token
The `expirationTimestamp` is set to ~1 second from now, which effectively disables kubectl's exec credential cache. This ensures concurrent kubectl processes (some normal, some under `with-prod`) always get the correct token. The metadata proxy already caches tokens, so the overhead is one fast localhost HTTP round-trip per kubectl API call.
**Why not `gke-gcloud-auth-plugin`?** The GKE plugin caches tokens at `~/.kube/gke_gcloud_auth_plugin_cache` and ignores `CLOUDSDK_CONFIG`, so it keeps serving stale dev tokens even under `with-prod`.
### `version` — Show version
Prints the current version and exits.
```bash
gcp-authcalator version
gcp-authcalator --version
```
## Devcontainer setup
To use gcp-authcalator in a devcontainer:
1. **Host:** Start the `gate` daemon (e.g., in a devcontainer lifecycle script that runs on the host):
```bash
gcp-authcalator gate --config /path/to/config.toml
```
2. **Mount the socket** into the container by adding to `devcontainer.json`.
The socket lives in a user-private directory — use `$XDG_RUNTIME_DIR` (typically `/run/user/$UID`) or `~/.gcp-authcalator/` if that's unset:
```json
"mounts": [
"source=${localEnv:XDG_RUNTIME_DIR}/gcp-authcalator.sock,target=${localEnv:XDG_RUNTIME_DIR}/gcp-authcalator.sock,type=bind"
]
```
Make sure the container uses the same `--socket-path` as the host.
3. **Container:** Start the metadata proxy (e.g., in a post-start script):
```bash
gcp-authcalator metadata-proxy --project-id my-project &
```
4. **Container:** Set the environment variables so client libraries discover the proxy:
```json
"remoteEnv": {
"GCE_METADATA_HOST": "127.0.0.1:8173",
"GCE_METADATA_IP": "127.0.0.1:8173"
}
```
5. **Container (optional):** If you use `kubectl` with GKE, patch the kubeconfig so kubectl fetches tokens through gcp-authcalator instead of `gke-gcloud-auth-plugin`:
```bash
gcloud container clusters get-credentials --region --project
gcp-authcalator kube-setup
```
This ensures `kubectl` works correctly under both normal and `with-prod` usage.
## Security model
### Threat model
gcp-authcalator is designed for environments where a coding agent (or other untrusted automation) runs in the same devcontainer as the engineer. The goal is to ensure that **all privilege escalation requires human approval** and that **credentials are never directly accessible inside the container**.
**Hard security boundaries:**
- **Credentials never enter the container.** The host daemon holds ADC; the container only receives short-lived, downscoped tokens. Even if the container is fully compromised, the attacker gets only a dev service account token — not the engineer's identity.
- **Cross-user isolation.** The Unix socket is set to `0600` (owner-only) and lives in a `0700` directory (`$XDG_RUNTIME_DIR` or `~/.gcp-authcalator/`). Processes running as other OS users cannot connect. **For strongest isolation, run coding agents as a separate OS user** — they will be unable to access the socket at all.
- **Human-in-the-loop for production access.** Prod tokens require explicit confirmation via a desktop dialog (`osascript` on macOS, `zenity` on Linux) or terminal prompt on the host. If no interactive method is available, access is denied.
- **Rate limiting** prevents automated brute-forcing of the confirmation flow: one dialog at a time, a 5-second cooldown after denial, and a maximum of 5 attempts per minute.
**Best-effort protections** (defense in depth against same-user attacks):
- **PID-based process restriction** on `with-prod` temporary proxies ensures only the intended process tree can request elevated tokens. This uses `/proc` introspection and is effective against casual abuse, but a sufficiently privileged same-user process could circumvent it.
- **Environment isolation** in `with-prod` strips credential-related env vars (`GOOGLE_APPLICATION_CREDENTIALS`, `CLOUDSDK_AUTH_ACCESS_TOKEN`, etc.) and uses a temporary `CLOUDSDK_CONFIG` in the user-private runtime directory to prevent credential leakage around the proxy.
- **Token files** are written with `0600` permissions in user-private directories, not passed via environment variables (which are readable via `/proc/*/environ`).
- **Audit logging** records all token requests as JSON lines to the runtime directory, providing a trail for forensic review.
- **Stale socket recovery** verifies socket ownership and refuses to follow symlinks, preventing TOCTOU races.
**Limitations:**
- A malicious process running as the **same user** with sufficient sophistication (e.g., `ptrace`, reading `/proc/*/mem`) can potentially extract tokens from a running process. Full same-user isolation requires OS-level sandboxing beyond what gcp-authcalator provides.
- Once the engineer approves a prod token request, the elevated token is available to the approved process tree for its lifetime (~1 hour). gcp-authcalator cannot revoke it early.
## Development
### Setup
```bash
mise install
bun install
prek install
```
### Pre-commit checks
```bash
bun run format # auto-fix formatting
bun run lint # run ESLint
bun run typecheck # check types
bun test # run tests
```
### Building
```bash
bun run build # build for current platform
bun run build:linux-amd64 # cross-compile for Linux x86_64
bun run build:darwin-arm64 # cross-compile for macOS ARM64
```
See [docs/releasing.md](docs/releasing.md) for the release process.