CI/CD pipeline access
This guide covers granting a CI/CD pipeline non-interactive access to an OGO-managed gateway — for example, a job that spins up a sandbox, runs an AI agent inside it, and tears it down as part of a larger pipeline.
Two supported paths, in order of preference:
- ServiceAccount +
OpenShellWorkspaceMember(recommended) — a dedicated, revocable identity per pipeline, authorized declaratively. - mTLS — simpler to set up, but a single shared client certificate grants access to anyone who has it; no per-identity revocation or audit trail. Reasonable for a quick start or a low-stakes pipeline; see mTLS in the Authentication concept doc.
The rest of this guide covers path 1.
1. Create a dedicated ServiceAccount
OpenShellWorkspaceMember's serviceAccountRef only resolves within its
own namespace (see Security)
— the ServiceAccount must live in the same namespace as the gateway,
not in your CI runner's own namespace.
oc create serviceaccount ci-pipeline -n ogo
Use a dedicated ServiceAccount per pipeline rather than reusing a shared or default one. A shared identity can't be individually revoked or audited, and over-grants access to every pipeline using it.
2. Grant workspace membership
apiVersion: gateway.ogo.aknochow.io/v1alpha1
kind: OpenShellWorkspaceMember
metadata:
name: ci-pipeline
namespace: ogo
spec:
workspace: default
serviceAccountRef:
name: ci-pipeline
role: user
oc apply -f openshellworkspacemember-ci-pipeline.yaml
oc get openshellworkspacemember ci-pipeline -n ogo -w
# wait for phase: Synced
See Workspace Membership for the full concept — this is the authorization layer; a valid token alone doesn't grant sandbox access without it.
3. Get the ServiceAccount's token into the pipeline
How you do this depends on whether your CI platform can run the pipeline's pod as this ServiceAccount directly:
Option A — the runner can bind to the ServiceAccount (preferred)
Some platforms let you run a job's pod under an arbitrary existing
ServiceAccount in a chosen namespace — for example, Tekton's
taskRunTemplate.serviceAccountName, or a Kubernetes-executor-based
runner configured with a target namespace and service account. When this
is possible, point the job at the ci-pipeline ServiceAccount created
above, running in the ogo namespace. Kubernetes automatically projects
that ServiceAccount's token into the pod at the standard path
(/var/run/secrets/kubernetes.io/serviceaccount/token) — no secret
management needed, and the token is short-lived and automatically
rotated.
Option B — store a durable token as a CI/CD variable
If your platform can't run the job as an arbitrary ServiceAccount (for example, its execution environment always runs under its own fixed identity, or automatic token projection is disabled and can't be overridden), generate a token for the ServiceAccount once and store it as a protected, masked CI/CD variable instead:
oc create token ci-pipeline -n ogo --duration=8760h
Store the output as a protected/masked variable in your CI platform
(e.g. a GitLab CI/CD variable, a Jenkins credential). Treat it like any
other long-lived credential: masked in logs, restricted to protected
branches/pipelines, and rotated periodically
(oc create token ci-pipeline -n ogo --duration=8760h again to reissue).
4. Exchange the token and configure the CLI
Whichever option above got you a Kubernetes ServiceAccount token, the
pipeline step itself is the same — exchange it for an OpenShell JWT via
auth-bridge's /token/exchange endpoint, then write the CLI's config
files directly (the same technique used for
Dev Spaces "with auth"):
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) # Option A
# or: SA_TOKEN="$CI_SERVICEACCOUNT_TOKEN" # Option B, from your CI variable
RESPONSE=$(curl -sf -X POST \
-H "Authorization: Bearer ${SA_TOKEN}" \
https://openshell-auth.apps.YOUR-CLUSTER.example.com/token/exchange)
GATEWAY_HOST="openshell.apps.YOUR-CLUSTER.example.com"
AUTH_HOST="openshell-auth.apps.YOUR-CLUSTER.example.com"
mkdir -p ~/.config/openshell/gateways/ci
cat > ~/.config/openshell/gateways/ci/metadata.json <<EOF
{
"name": "ci",
"gateway_endpoint": "https://${GATEWAY_HOST}",
"is_remote": true,
"gateway_port": 0,
"auth_mode": "oidc",
"oidc_issuer": "https://${AUTH_HOST}",
"oidc_client_id": "openshell-cli"
}
EOF
cat > ~/.config/openshell/gateways/ci/oidc_token.json <<EOF
{
"access_token": "$(echo "$RESPONSE" | jq -r .access_token)",
"expires_at": $(echo "$RESPONSE" | jq -r .expires_at),
"issuer": "https://${AUTH_HOST}",
"client_id": "openshell-cli"
}
EOF
echo ci > ~/.config/openshell/active_gateway
openshell sandbox create --no-keep -- your-command-here
If a pipeline step's token is close to its expiry (default JWT TTL is 8h, see Authentication), re-run the exchange rather than reusing a cached one — pipeline jobs are typically short-lived enough that this is a non-issue, but a long-running job should re-exchange partway through rather than assume the token outlives the job.
Same-cluster networking
If your CI runner's pods run on the same cluster as the OGO-managed
gateway, you can talk to the gateway's in-cluster Service directly instead
of routing through the public Route — skips the router hop entirely. Check
whether the gateway's direct in-cluster listener has TLS enabled
(spec.tls.enabled on the OpenShellGateway CR) before choosing http://
vs https://; sending a TLS handshake to a plaintext-only listener (or
vice versa) fails immediately with a protocol-level error, not a helpful
one.
See also
- Authentication
- Workspace Membership
- OpenShellWorkspaceMember reference
- Shared Volumes — mounting existing PVCs into sandboxes created from a pipeline
- Tekton Shared Workspace example