# OpenShift

> How to install Capsule and the Capsule Proxy on OpenShift

> For the complete documentation index, see [llms.txt](https://projectcapsule.dev/llms.txt).
> The HTML version of this page is at https://projectcapsule.dev/docs/operating/setup/openshift/.

## Introduction

Capsule is a Kubernetes multi-tenancy operator that enables secure namespace-as-a-service in Kubernetes clusters. When combined with OpenShift's robust security model, it provides an excellent platform for multi-tenant environments.

This guide demonstrates how to deploy Capsule and Capsule Proxy on OpenShift using the `nonroot-v2` and `restricted-v2` [SecurityContextConstraint (SCC)](https://docs.redhat.com/en/documentation/openshift_container_platform/4.21/html/authentication_and_authorization/managing-pod-security-policies), ensuring tenant owners operate within OpenShift's security boundaries.

## Why Capsule on OpenShift
While OpenShift can already be configured for multi-tenancy (for example with Kyverno), Capsule takes it a step further and makes it easier to manage.

When people think of a multi-tenant Kubernetes cluster, they often expect one or two namespaces with few privileges. Capsule, however, is different. As a tenant owner, you can create as many namespaces as you want. RBAC is much easier because Capsule handles it, making it less error-prone. Resource quotas are not set per namespace but are spread across the whole tenant, simplifying management. Capsule Proxy also solves RBAC issues when listing cluster-wide resources. Furthermore, some operators can be installed inside a tenant by using the [Capsule Proxy](/docs/proxy/): add the service account as a tenant owner and set the `KUBERNETES_SERVICE_HOST` environment variable of the operator deployment to the Capsule Proxy URL. The operator then behaves as if it has cluster-admin access, while remaining fully confined to the tenant.

## Prerequisites
Before starting, ensure you have:
- OpenShift cluster with cluster-admin privileges
- `kubectl` CLI configured
- Helm 3.x installed
- cert-manager installed

## Limitations
The following limitations are known when using OpenShift with Capsule:
- A tenant owner cannot create a namespace/project in the OpenShift GUI. This must be done with `kubectl`.
- When copying the `login token` from the OpenShift GUI, the server address will always point to the Kubernetes API instead of the Capsule Proxy, so the `--server` flag has to be adjusted manually. Browser-based `oc login --web` against the Capsule Proxy works without this workaround when public paths are configured (see [Capsule Proxy](#capsule-proxy)). An RFE has been filed with Red Hat to make this URL configurable ([RFE-7592](https://issues.redhat.com/browse/RFE-7592)). If you have a support contract with Red Hat, consider opening a support request (SR) asking for this feature. The more requests there are, the higher the priority.

## Capsule Installation
### Remove the self-provisioners ClusterRoleBinding
By default, OpenShift includes a self-provisioner role and ClusterRoleBinding that allows all users to create namespaces. Capsule requires this to be removed. See the [Red Hat documentation](https://docs.redhat.com/en/documentation/openshift_container_platform/4.21/html/building_applications/projects#disabling-project-self-provisioning_configuring-project-creation) for details.

Remove the subjects from the ClusterRoleBinding:
```shell
kubectl patch clusterrolebinding.rbac self-provisioners -p '{"subjects": null}'
```
Also set `autoupdate` to false so the ClusterRoleBinding is not reverted by OpenShift.
```shell
kubectl patch clusterrolebinding.rbac self-provisioners -p '{ "metadata": { "annotations": { "rbac.authorization.kubernetes.io/autoupdate": "false" } } }'
```

### Extend the admin role
This example extends the default Kubernetes `admin` role so tenant owners gain admin privileges on all namespaces within their tenant. The extension adds:
- The finalizers required to create/edit resources managed by Capsule
- The SCCs that tenant owners can use (in this example, `restricted-v2` and `nonroot-v2`)

```yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: extend-admin-role
  labels:
    rbac.authorization.k8s.io/aggregate-to-admin: 'true'
rules:
  - verbs:
      - update
    apiGroups:
      - capsule.clastix.io
    resources:
      - '*/finalizers'
  - apiGroups:
      - security.openshift.io
    resources:
      - securitycontextconstraints
    resourceNames:
      - restricted-v2
      - nonroot-v2
    verbs:
      - 'use'
```

### Helm Chart Values
The jobs that Capsule uses can be run with the `restricted-v2` SCC, so their `securityContext` and `podSecurityContext` must be disabled. For Capsule itself, they are left enabled because Capsule runs as `nonroot-v2`, which is still a very secure SCC. Always set `pullPolicy: Always` on a multi-tenant cluster to ensure the intended images are used.
The following chart values can be used:
```yaml
  podSecurityContext:
    enabled: true
  securityContext:
    enabled: true
  jobs:
    podSecurityContext:
      enabled: false
    securityContext:
      enabled: false
    image:
      pullPolicy: Always
  manager:
    image:
      pullPolicy: Always
```
Deploy the Capsule Helm chart with (at least) these values.

### etcd Encryption

If etcd encryption is enabled on your OpenShift cluster and you are using `TenantResource` or `GlobalTenantResource` to replicate `ConfigMap`s or `Secret`s, the Capsule replication webhook must be configured to skip requests from OpenShift's storage version migrator. Without this exclusion, the migrator, which iterates over all Secrets and ConfigMaps to rotate the encryption key, will be blocked or cause reconciliation errors, because the webhook intercepts its requests.

Add the following `matchCondition` to the replication webhook via the Helm chart values:

```yaml
webhooks:
  hooks:
    replications:
      matchConditions:
        - name: "exclude-privileged-users"
          expression: >
            !(
              request.userInfo.username in [
                "system:serviceaccount:openshift-kube-storage-version-migrator:kube-storage-version-migrator-sa"
              ]
            )
```

### Example Tenant and TenantOwners

A minimal example tenant looks like the following:

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  imagePullPolicies:
    - Always
  permissions:
    matchOwners:
      - matchLabels:
          team: devops
  priorityClasses:
    allowed:
      - openshift-user-critical
  rules:
    - audience:
        - kind: Custom
          name: "CapsuleUser"
      enforce:
        action: deny
        metadata:
          - apiGroups:
              - "v1"
            kinds:
              - "Namespace"
            labels:
              ".*openshift.io/.*":
                required: false
              "pod-security.kubernetes.io/enforce":
                required: false
            annotations:
              ".*openshift.io/.*":
                required: false
              "pod-security.kubernetes.io/enforce":
                required: false

```


> **Block openshift labels and annotations**
>
> Make sure that CapsuleUsers cannot modify OpenShift-managed labels and annotations. These metadata fields should be mutable only by OpenShift-managed components. For example, modifying the openshift.io/run-level label on a namespace can allow a user to bypass SecurityContextConstraints and create privileged workloads. Similarly, the openshift.io/sa.scc.uid-range annotation controls important SecurityContextConstraints configuration and must not be user-modifiable. The pod-security.kubernetes.io/enforce label should also be protected because it is managed by OpenShift based on the SCCs. See the <a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.21/html/authentication_and_authorization/managing-pod-security-policies#security-context-constraints-about_configuring-internal-oauth">Red Hat documentation</a> for more information.


Combined with a `TenantOwner` resource to grant access to the tenant:

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantOwner
metadata:
  labels:
    team: devops
  name: devops
spec:
  kind: Group
  name: "oidc:org:devops:a"
```

More information about tenants and tenant owners can be found in the chapter [Tenants](/docs/tenants/).

## Capsule Proxy
For Capsule Proxy, all (pod)SecurityContexts can be disabled. By disabling these, the proxy and its jobs run under the `nonroot-v2` SCC.

The option `--public-paths=/.well-known/oauth-authorization-server` (available since capsule-proxy v0.14.1) is set, which allows `oc login --web` to be used against the Capsule Proxy. The `oc login --web` flow retrieves the OAuth server metadata from `/.well-known/oauth-authorization-server` on the API endpoint before any credentials exist. Since Capsule Proxy requires authentication on every request, this lookup is rejected by default and the login aborts with `unsupported protocol scheme ""`. Public paths solve this: requests to a public path are proxied to the upstream API server without authentication, while everything else keeps requiring one.

The following helm values can be used as a template:

```yaml
  global:
    jobs:
      kubectl:
        securityContext:
          enabled: false
  securityContext:
    enabled: false
  podSecurityContext:
    enabled: false
  options:
    generateCertificates: false #set to false, since we are using cert-manager in .Values.certManager.generateCertificates
    enableSSL: true
    extraArgs:
      - '--public-paths=/.well-known/oauth-authorization-server'
  image:
    pullPolicy: Always
  webhooks:
    enabled: true
  certManager:
    generateCertificates: true
  ingress:
    enabled: true
    annotations:
      route.openshift.io/termination: "reencrypt"
      route.openshift.io/destination-ca-certificate-secret: capsule-proxy-root-secret
    hosts:
      - host: "capsule-proxy.example.com"
        paths: ["/"]
```
That is all the configuration needed for Capsule Proxy.

### ProxyClusterScoped and OpenShift Projects

If the `ProxyClusterScoped` feature gate is enabled (not used in the example above, but is is a option to enable), it is recommended to create a `GlobalProxySetting` for `Projects`. OpenShift `Projects` are the equivalent of Kubernetes `Namespaces`, and when `ProxyClusterScoped` is active they can no longer be listed by default.

```yaml
apiVersion: capsule.clastix.io/v1beta1
kind: GlobalProxySettings
metadata:
  name: openshift-projects
spec:
  rules:
  - subjects:
    - kind: Group
      name: oidc:org:devops:a
    clusterResources:
    - apiGroups:
      - "project.openshift.io"
      resources:
      - "projects"
      operations:
      - List
      selector:
        matchLabels:
            capsule.clastix.io/tenant: solar
```

### Hosted control planes (HyperShift)

On hosted control planes each hosted cluster runs its own OAuth server, exposed on its own route (for example, `oauth-clusters-<cluster-name>.<base-domain>`) with a certificate signed by that cluster's root CA.

The `--public-paths=/.well-known/oauth-authorization-server` option described in the [Capsule Proxy](#capsule-proxy) section above also makes `oc login <capsule-proxy-host> --web` work against a HyperShift-hosted cluster: the browser login and the token exchange run against the hosted cluster's OAuth server, and the API traffic goes through the proxy as usual.

The only extra step here is trusting the certificate: since the browser and the token exchange talk to the hosted cluster's OAuth server directly, the client must import that hosted cluster's root CA first, the same procedure as when logging in against the API server directly.

## Console Customization
The OpenShift console can be customized. For example, the capsule-proxy can be added as a shortcut on the top right application menu with the `ConsoleLink` CR:
```yaml
apiVersion: console.openshift.io/v1
kind: ConsoleLink
metadata:
  name: capsule-proxy-consolelink
spec:
  applicationMenu:
    imageURL: 'https://github.com/projectcapsule/capsule/raw/main/assets/logo/capsule.svg'
    section: 'Capsule'
  href: 'https://capsule-proxy.example.com'
  location: ApplicationMenu
  text: 'Capsule Proxy Kubernetes API'
```
It's also possible to add links specific for certain namespaces, which are shown on the Namespace/Project overview. These can also be tenant specific by adding a NamespaceSelector:
```yaml
apiVersion: console.openshift.io/v1
kind: ConsoleLink
metadata:
  name: namespaced-consolelink-solar
spec:
  text: "solar Docs"
  href: "https://linktothesolardocs.com"
  location: "NamespaceDashboard"
  namespaceDashboard:
    namespaceSelector:
      matchExpressions:
        - key: capsule.clastix.io/tenant
          operator: In
          values:
            - solar
```
Also a custom logo can be provided, for example by adding the Capsule logo.

**Add** these config lines to the existing `cluster` CR `Console`.
```shell
kubectl create configmap console-capsule-logo --from-file capsule-logo.png -n openshift-config
```
```yaml
apiVersion: operator.openshift.io/v1
kind: Console
metadata:
  name: cluster
spec:
  customization:
    customLogoFile:
      key: capsule-logo.png
      name: console-capsule-logo
    customProductName: Capsule OpenShift Cluster
```

# Conclusion
You now have a fully configured Capsule and Capsule Proxy installation on OpenShift, including console customizations, and the environment is ready to hand off to development teams.

---

Last modified September 29, 2026: chore(openshift): move oauth-auth url to proxy installation section (#144) (6a8cd39)
