Skip to content
Open
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
38 changes: 38 additions & 0 deletions deploy/docker/Dockerfile.gateway.openshift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1.4

# Multi-stage gateway build for OpenShift BuildConfig.
# Compiles openshell-gateway from source with bundled Z3.

FROM registry.access.redhat.com/ubi9/ubi:latest AS builder

RUN dnf install -y --setopt=tsflags=nodocs \
gcc-toolset-15-gcc gcc-toolset-15-gcc-c++ gcc-toolset-15-libstdc++-devel \
gcc-toolset-15-libatomic-devel gcc-toolset-15-binutils \
make cmake openssl-devel pkg-config perl-FindBin perl-File-Compare \
clang-libs clang-devel llvm-devel \
&& dnf clean all

ENV PATH="/opt/rh/gcc-toolset-15/root/usr/bin:${PATH}" \
CC=gcc CXX=g++ \
LD_LIBRARY_PATH="/opt/rh/gcc-toolset-15/root/usr/lib64"

RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain 1.95.0 --profile minimal
ENV PATH="/root/.cargo/bin:/opt/rh/gcc-toolset-15/root/usr/bin:${PATH}"

WORKDIR /build
COPY . .

RUN cargo build --release -p openshell-server --features bundled-z3 \
&& strip target/release/openshell-gateway

# --- runtime ---
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest

COPY --from=builder /build/target/release/openshell-gateway /usr/local/bin/openshell-gateway

USER 1000:1000
EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/openshell-gateway"]
CMD ["--bind-address", "0.0.0.0", "--port", "8080"]
42 changes: 42 additions & 0 deletions deploy/docker/Dockerfile.supervisor.openshift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# syntax=docker/dockerfile:1.4

# Multi-stage supervisor build for OpenShift BuildConfig.
# Compiles a static musl binary of openshell-sandbox.

FROM registry.access.redhat.com/ubi9/ubi:latest AS builder

RUN dnf install -y --setopt=tsflags=nodocs \
gcc gcc-c++ make openssl-devel pkg-config \
perl-FindBin perl-File-Compare \
&& dnf clean all

RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain 1.95.0 --profile minimal \
&& /root/.cargo/bin/rustup target add x86_64-unknown-linux-musl
ENV PATH="/root/.cargo/bin:${PATH}"

# Install musl-gcc (not in UBI repos, build from source)
RUN curl -fsSL https://musl.libc.org/releases/musl-1.2.5.tar.gz | tar xz \
&& cd musl-1.2.5 \
&& ./configure --prefix=/usr/local/musl --disable-shared \
&& make -j$(nproc) && make install \
&& cd .. && rm -rf musl-1.2.5 \
&& ln -s /usr/local/musl/bin/musl-gcc /usr/local/bin/musl-gcc

ENV CC_x86_64_unknown_linux_musl=musl-gcc

WORKDIR /build
COPY . .

RUN cargo build --release -p openshell-sandbox \
--target x86_64-unknown-linux-musl \
&& strip target/x86_64-unknown-linux-musl/release/openshell-sandbox

# --- runtime ---
FROM scratch

COPY --chmod=0550 --from=builder \
/build/target/x86_64-unknown-linux-musl/release/openshell-sandbox \
/openshell-sandbox

ENTRYPOINT ["/openshell-sandbox"]
254 changes: 252 additions & 2 deletions docs/kubernetes/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# SPDX-License-Identifier: Apache-2.0
title: "Access Control"
sidebar-title: "Access Control"
description: "Configure OIDC user authentication or reverse-proxy auth termination for a Kubernetes-deployed OpenShell gateway."
keywords: "Generative AI, Cybersecurity, Kubernetes, Authentication, mTLS, OIDC, Keycloak, Entra ID, Okta, Gateway Auth"
description: "Configure OIDC user authentication, Keycloak setup, OpenShift identity federation, or reverse-proxy auth termination for a Kubernetes-deployed OpenShell gateway."
keywords: "Generative AI, Cybersecurity, Kubernetes, Authentication, mTLS, OIDC, Keycloak, Entra ID, Okta, Gateway Auth, OpenShift, Identity Federation"
position: 5
---

Expand Down Expand Up @@ -82,6 +82,256 @@ Both `adminRole` and `userRole` must be set, or both must be empty. Setting only
| Microsoft Entra ID | `roles` |
| Okta | `groups` |

### Keycloak setup

Keycloak public clients do not include `sub`, `aud`, or realm roles in access tokens by default. Without these claims, the gateway rejects tokens with errors like `missing field 'sub'`, audience mismatch, or `role 'openshell-user' required`.

After creating a realm and a public client (with PKCE S256, redirect URIs `http://localhost:*` and `http://127.0.0.1:*`), add these protocol mappers to the client:

| Mapper name | Mapper type | Key config |
|---|---|---|
| `sub` | Subject (sub) | access.token.claim: true |
| `openshell-audience` | Audience | included.client.audience: `openshell-cli` |
| `realm-roles` | User Realm Role | claim.name: `realm_access.roles`, multivalued: true |

Add these via the Keycloak admin console under **Clients → openshell-cli → Client scopes → Dedicated scope → Add mapper**, or via the CLI:

<Tabs>
<Tab title="kcadm.sh">

```shell
KC_ADM="kcadm.sh --config /tmp/kcadm.config"

$KC_ADM config credentials \
--server http://localhost:8080 \
--realm master \
--user <admin-user> \
--password <admin-password>

$KC_ADM create realms \
-s realm=openshell \
-s enabled=true

$KC_ADM create clients -r openshell \
-s clientId=openshell-cli \
-s enabled=true \
-s publicClient=true \
-s directAccessGrantsEnabled=true \
-s standardFlowEnabled=true \
-s 'redirectUris=["http://localhost:*","http://127.0.0.1:*"]' \
-s 'webOrigins=["http://localhost","http://127.0.0.1"]' \
-s 'attributes={"pkce.code.challenge.method":"S256"}'

CLIENT_UUID=$($KC_ADM get clients -r openshell \
-q clientId=openshell-cli --fields id --format csv --noquotes)

$KC_ADM create clients/$CLIENT_UUID/protocol-mappers/models -r openshell \
-s name=sub \
-s protocol=openid-connect \
-s protocolMapper=oidc-sub-mapper \
-s 'config={"access.token.claim":"true","id.token.claim":"true"}'

$KC_ADM create clients/$CLIENT_UUID/protocol-mappers/models -r openshell \
-s name=openshell-audience \
-s protocol=openid-connect \
-s protocolMapper=oidc-audience-mapper \
-s 'config={"included.client.audience":"openshell-cli","access.token.claim":"true","id.token.claim":"true"}'

$KC_ADM create clients/$CLIENT_UUID/protocol-mappers/models -r openshell \
-s name=realm-roles \
-s protocol=openid-connect \
-s protocolMapper=oidc-usermodel-realm-role-mapper \
-s 'config={"claim.name":"realm_access.roles","jsonType.label":"String","multivalued":"true","access.token.claim":"true","id.token.claim":"true","userinfo.token.claim":"true"}'

$KC_ADM create roles -r openshell -s name=openshell-user
$KC_ADM create roles -r openshell -s name=openshell-admin
```

</Tab>
<Tab title="REST API">

```shell
KC_URL=https://keycloak.example.com

TOKEN=$(curl -sk "${KC_URL}/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=<admin-user>" \
-d "password=<admin-password>" \
-d "grant_type=password" | jq -r .access_token)

curl -sk -X POST "${KC_URL}/admin/realms" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"realm":"openshell","enabled":true}'

curl -sk -X POST "${KC_URL}/admin/realms/openshell/clients" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientId": "openshell-cli",
"enabled": true,
"publicClient": true,
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"redirectUris": ["http://localhost:*", "http://127.0.0.1:*"],
"webOrigins": ["http://localhost", "http://127.0.0.1"],
"attributes": {"pkce.code.challenge.method": "S256"}
}'

CLIENT_UUID=$(curl -sk "${KC_URL}/admin/realms/openshell/clients?clientId=openshell-cli" \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id')

for MAPPER in \
'{"name":"sub","protocol":"openid-connect","protocolMapper":"oidc-sub-mapper","config":{"access.token.claim":"true","id.token.claim":"true"}}' \
'{"name":"openshell-audience","protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","config":{"included.client.audience":"openshell-cli","access.token.claim":"true","id.token.claim":"true"}}' \
'{"name":"realm-roles","protocol":"openid-connect","protocolMapper":"oidc-usermodel-realm-role-mapper","config":{"claim.name":"realm_access.roles","jsonType.label":"String","multivalued":"true","access.token.claim":"true","id.token.claim":"true","userinfo.token.claim":"true"}}'; do
curl -sk -X POST "${KC_URL}/admin/realms/openshell/clients/${CLIENT_UUID}/protocol-mappers/models" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$MAPPER"
done

curl -sk -X POST "${KC_URL}/admin/realms/openshell/roles" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"openshell-user"}'

curl -sk -X POST "${KC_URL}/admin/realms/openshell/roles" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"openshell-admin"}'
```

</Tab>
</Tabs>

Assign `openshell-user` to users who need sandbox access and `openshell-admin` to administrators.

<Note>
On Kubernetes, run `kcadm.sh` via `kubectl exec` into the Keycloak pod. On OpenShift, use `oc exec`. The `--config /tmp/kcadm.config` flag is required when the container runs as non-root. For OpenShift-specific OIDC gateway configuration (Helm upgrade with OIDC values and CLI registration), see the [OpenShift](/kubernetes/openshift) install guide.
</Note>

### OpenShift Identity Federation (optional)

If your organisation wants OpenShell users to authenticate with their existing OpenShift cluster credentials instead of managing separate Keycloak accounts, configure Keycloak to federate to the OpenShift OAuth server. This is optional — the [Keycloak setup](#keycloak-setup) above works standalone with its own user directory.

When federation is configured, users are auto-imported into the Keycloak realm on first login.

**Prerequisites:** [Keycloak setup](#keycloak-setup) completed and working, `cluster-admin` access to create `OAuthClient` resources.

#### Create an OAuthClient in OpenShift

Generate a client secret and create the `OAuthClient` that Keycloak uses to initiate the OAuth flow:

```shell
OCP_SECRET=$(openssl rand -hex 32)
KC_URL=https://keycloak.example.com

cat <<EOF | oc apply -f -
apiVersion: oauth.openshift.io/v1
kind: OAuthClient
metadata:
name: keycloak-openshell
secret: "${OCP_SECRET}"
grantMethod: auto
redirectURIs:
- "${KC_URL}/realms/openshell/broker/openshift/endpoint"
EOF
```

#### Add OpenShift as a Keycloak identity provider

<Tabs>
<Tab title="kcadm.sh">

```shell
KC_POD="oc exec keycloak-0 -n keycloak --"
KC_ADM="/opt/keycloak/bin/kcadm.sh --config /tmp/kcadm.config"

$KC_POD $KC_ADM create identity-provider/instances -r openshell \
-s alias=openshift \
-s displayName=OpenShift \
-s providerId=openshift-v4 \
-s enabled=true \
-s trustEmail=true \
-s "firstBrokerLoginFlowAlias=first broker login" \
-s "config.clientId=keycloak-openshell" \
-s "config.clientSecret=${OCP_SECRET}" \
-s "config.baseUrl=$(oc whoami --show-server)" \
-s "config.defaultScope=user:info" \
-s "config.syncMode=IMPORT"
```

</Tab>
<Tab title="REST API">

```shell
TOKEN=$(curl -sk "${KC_URL}/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=<admin-user>" \
-d "password=<admin-password>" \
-d "grant_type=password" | jq -r .access_token)

curl -sk -X POST "${KC_URL}/admin/realms/openshell/identity-provider/instances" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"alias\": \"openshift\",
\"displayName\": \"OpenShift\",
\"providerId\": \"openshift-v4\",
\"enabled\": true,
\"trustEmail\": true,
\"firstBrokerLoginFlowAlias\": \"first broker login\",
\"config\": {
\"clientId\": \"keycloak-openshell\",
\"clientSecret\": \"${OCP_SECRET}\",
\"baseUrl\": \"$(oc whoami --show-server)\",
\"defaultScope\": \"user:info\",
\"syncMode\": \"IMPORT\"
}
}"
```

</Tab>
</Tabs>

<Note>
**ROSA HCP**: set `baseUrl` to the API server URL (`https://api.<cluster>:443`), not the OAuth route (`oauth.<cluster>`). On ROSA HCP, the OAuth metadata endpoint (`/.well-known/oauth-authorization-server`) is served by the API server. The OAuth route returns 404.
</Note>

#### Assign roles to federated users

Users imported from OpenShift do not receive the `openshell-user` or `openshell-admin` realm roles automatically. After a user's first login through the "OpenShift" button, assign the appropriate role:

**Keycloak Admin Console → Users → select user → Role Mappings → Assign role → openshell-user**

Or via the CLI/API:

<Tabs>
<Tab title="kcadm.sh">

```shell
$KC_POD $KC_ADM add-roles -r openshell \
--uusername <openshift-username> \
--rolename openshell-user
```

</Tab>
<Tab title="REST API">

```shell
USER_ID=$(curl -sk "${KC_URL}/admin/realms/openshell/users?username=<openshift-username>&exact=true" \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id')

ROLE_ID=$(curl -sk "${KC_URL}/admin/realms/openshell/roles/openshell-user" \
-H "Authorization: Bearer $TOKEN" | jq -r '.id')

curl -sk -X POST "${KC_URL}/admin/realms/openshell/users/${USER_ID}/role-mappings/realm" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "[{\"id\":\"${ROLE_ID}\",\"name\":\"openshell-user\"}]"
```

</Tab>
</Tabs>

The Keycloak login page shows a "Login with OpenShift" button below the standard username/password form. After authentication, Keycloak issues an OIDC token with the user's identity and assigned roles.

## Reverse-Proxy Auth Termination

When an access proxy, such as Cloudflare Access, ngrok, or a corporate SSO gateway, handles authentication in front of the OpenShell gateway, you can explicitly allow unauthenticated user calls at the gateway:
Expand Down
Loading
Loading