# Adding Custom CA Certificates

When UDiTH Portal needs to communicate over HTTPS with keycloak or a database
that use a certificate issued by a **company-internal Certificate
Authority (CA)**, the CA certificate must be trusted **inside the container**.

Portal runs on .NET, which on Linux uses the operating system's OpenSSL trust
store. Adding your CA to the system trust store therefore makes it trusted by
Portal as well. There is no Portal-specific certificate setting — it is purely an
operating-system-level configuration of the base image.

The `quay.io/caxperts/portal` image is published with several base-OS variants.
The procedure differs slightly per variant:

| Tag              | Base OS              | Runs as       | Tooling                | Trust bundle used by .NET                  |
| ---------------- | -------------------- | ------------- | ---------------------- | ------------------------------------------ |
| `noble`          | Ubuntu         | root          | `update-ca-certificates` | `/etc/ssl/certs/ca-certificates.crt`     |
| `ubi`            | RHEL 9 (UBI)         | uid 1001      | `update-ca-trust`      | `/etc/pki/tls/certs/ca-bundle.crt`         |
| `alpine`         | Alpine Linux     | root          | `update-ca-certificates` *(must be installed)* | `/etc/ssl/certs/ca-certificates.crt` |
| `noble-chiseled` | Ubuntu chiseled | uid 1654     | *none (no shell)*      | `/etc/ssl/certs/ca-certificates.crt`       |

> **Recommended approach:** Build a small derived image based on the Portal image
> that adds your CA certificate at build time. This works for **all** variants
> (including `noble-chiseled`, which has no shell or package manager), produces an
> immutable, reproducible image, and requires no privileged runtime configuration.

## Prerequisites

- Your company-internal **CA certificate** in PEM format (a text file beginning
  with `-----BEGIN CERTIFICATE-----`). The file extension should be `.crt`.
  - If your CA is in DER/binary format, convert it first:
    ```bash
    openssl x509 -inform DER -in company-ca.der -out company-ca.crt
    ```
  - If there is an intermediate CA in the chain, add it as a separate `.crt`
    file as well.
- Docker available on the machine that builds the image.

Place the CA file (here called `company-ca.crt`) next to the `Dockerfile` you
create below.

## Portal images (build-time approach, recommended)

Create a `Dockerfile` for your variant, then build a derived image and use that
image instead of the original `quay.io/caxperts/portal` image in your
`docker-compose.yml`.

### `noble` (Ubuntu)

```dockerfile
FROM quay.io/caxperts/portal:noble
USER root
COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
RUN update-ca-certificates
```

### `ubi` (Red Hat UBI)

```dockerfile
FROM quay.io/caxperts/portal:ubi
USER root
COPY company-ca.crt /etc/pki/ca-trust/source/anchors/company-ca.crt
RUN update-ca-trust
# Restore the original non-root runtime user
USER 1001
```

### `alpine`

Alpine does not ship the `ca-certificates` package by default, so it is installed
as part of the build:

```dockerfile
FROM quay.io/caxperts/portal:alpine
USER root
RUN apk add --no-cache ca-certificates
COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
RUN update-ca-certificates
```

### `noble-chiseled`

The chiseled image is a minimal (distroless-style) image with **no shell and no
package manager**, so the trust store cannot be updated inside it directly.
Instead, use a multi-stage build: regenerate the trust bundle in the full `noble`
image and copy the finished bundle into the chiseled image.

```dockerfile
# Stage 1: build the updated trust bundle using the full Ubuntu image
FROM quay.io/caxperts/portal:noble AS certbuilder
USER root
COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
RUN update-ca-certificates

# Stage 2: the actual chiseled runtime image with the updated bundle
FROM quay.io/caxperts/portal:noble-chiseled
COPY --from=certbuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
```

### Building the image

From the folder containing the `Dockerfile` and `company-ca.crt`:

```bash
docker build -t portal-custom-ca:noble .
```

Then reference the new image tag in your `docker-compose.yml`:

```yml
services:
  portal_demo:
    image: portal-custom-ca:noble
    # ... remaining configuration unchanged ...
```

If you maintain an internal container registry, tag and push the derived image
there so all your hosts can pull it:

```bash
docker tag portal-custom-ca:noble registry.example.com/udith/portal-custom-ca:noble
docker push registry.example.com/udith/portal-custom-ca:noble
```

## Windows (non-container) installations

The container variants above apply to the Linux/Docker deployment. The **Windows**
version of UDiTH Portal does not use these bundles — .NET on Windows uses the
**Windows certificate store**. To trust a company-internal CA there, import the CA
certificate into the **Trusted Root Certification Authorities** store of the
**Local Machine** (not the current user), so the Portal service account trusts it.

Using PowerShell (run as Administrator):

```powershell
Import-Certificate -FilePath C:\path\to\company-ca.crt `
  -CertStoreLocation Cert:\LocalMachine\Root
```

Alternatively, using `certutil`:

```cmd
certutil -addstore -f Root C:\path\to\company-ca.crt
```

Or via the GUI: run `certlm.msc` → **Trusted Root Certification Authorities** →
**Certificates** → right-click → **All Tasks** → **Import**, then select the CA
file. Import an intermediate CA into the **Intermediate Certification
Authorities** store. Restart the Portal service afterwards so it picks up the new
trust.

## Keycloak server (`quay.io/keycloak/keycloak`)

If you run your own Keycloak and it must trust a company-internal CA (for example
to call an internal LDAP/AD over LDAPS, an internal identity broker, or an SMTP
server over TLS), the certificate has to be trusted by **Keycloak**, not by
Portal.

Keycloak is a **Java** application, so it does **not** use the OpenSSL system
bundle that the Portal images use. It uses the **Keycloak System Truststore**.
The Keycloak 26.6.1 image is based on a *minimized* RHEL 9 UBI and runs as the
non-root user `keycloak` (uid 1000).

> The existing Java default truststore certificates are always trusted. You only
> need the steps below for self-signed certificates or internal Certificate
> Authorities that the JRE does not recognise.

### Keycloak System Truststore (recommended, no rebuild)

Keycloak automatically loads any certificate placed in the
`conf/truststores` directory (or its subdirectories, scanned recursively) into
its **System Truststore** at startup. This becomes both the default
`javax.net.ssl` truststore and the default for Keycloak's internal usage.

Supported file formats in `conf/truststores`:

- **PEM** files (a text file beginning with `-----BEGIN CERTIFICATE-----`).
- **PKCS12** files with extension `.p12`, `.pfx`, or `.pkcs12`. These must be
  **unencrypted** — no password is expected.

Mount your CA certificate into that folder:

```yml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:x.y.z
    command: start
    volumes:
      - ./company-ca.crt:/opt/keycloak/conf/truststores/company-ca.crt:ro
    # ... remaining configuration unchanged ...
```

You can place multiple files in the folder (for example a root and an
intermediate CA). On startup Keycloak logs that it has loaded them:

```
INFO  [org.keycloak.truststore.TruststoreBuilder] Found the following truststore files
in the truststore paths [/opt/keycloak/conf/truststores/company-ca.crt]
```

### Alternative — import into the Java `cacerts` at build time

Alternatively, bake the CA into the JVM default truststore (`cacerts`) using
`keytool`. Use this only if you specifically need the CA trusted by the JVM
default store rather than via the Keycloak System Truststore above.

```dockerfile
FROM quay.io/keycloak/keycloak:x.y.z
USER root
COPY company-ca.crt /tmp/company-ca.crt
RUN keytool -importcert -noprompt -trustcacerts -alias company-internal-ca \
    -file /tmp/company-ca.crt -cacerts -storepass changeit \
    && rm /tmp/company-ca.crt
USER 1000
```

Build it and reference the derived image instead of the original:

```bash
docker build -t keycloak-custom-ca:26.6.1 .
```

> `changeit` is the well-known default password of the Java `cacerts` store; it is
> not a secret. The `-cacerts` flag targets the JVM's default truststore
> automatically, so you do not need to know its path.

## Verifying the certificate is trusted

After building (or starting the container), confirm the CA is present in the
trust bundle.

For variants with a shell (`noble`, `ubi`, `alpine`):

```bash
# noble / alpine
docker run --rm --entrypoint sh portal-custom-ca:noble -c \
  "awk -v RS='-----END CERTIFICATE-----' '/Company Internal CA/{print \"found\"}' \
   /etc/ssl/certs/ca-certificates.crt; \
   openssl x509 -in /usr/local/share/ca-certificates/company-ca.crt -noout -subject"

# ubi
docker run --rm --entrypoint sh portal-custom-ca:ubi -c \
  "grep -c 'Company Internal CA' /etc/pki/tls/certs/ca-bundle.crt"
```

For `noble-chiseled` (no shell), copy the bundle out and inspect it on the host:

```bash
id=$(docker create portal-custom-ca:chiseled)
docker cp "$id:/etc/ssl/certs/ca-certificates.crt" ./bundle.crt
docker rm "$id"
grep -c "BEGIN CERTIFICATE" ./bundle.crt   # bundle now includes your CA
```

The most reliable functional test is to let Portal connect to your internal
HTTPS endpoint (for example Keycloak). If the CA is trusted, the previous TLS
errors such as `The remote certificate is invalid according to the validation
procedure` / `unable to get local issuer certificate` disappear from the Portal
logs.

For **Keycloak**, when using the System Truststore (`conf/truststores`) check the
startup log — Keycloak lists each loaded certificate at debug level:

```bash
docker run --rm \
  -v "$PWD/company-ca.crt:/opt/keycloak/conf/truststores/company-ca.crt:ro" \
  quay.io/keycloak/keycloak:26.6.1 \
  start-dev --log-level=org.keycloak.truststore:debug 2>&1 | grep -i "truststore"
# Expect: "Trusted root CA found in truststore ... Subject DN : CN=<your CA>"
```

When using the `cacerts` build-time method instead, confirm the import:

```bash
docker run --rm --entrypoint sh keycloak-custom-ca:26.6.1 -c \
  "keytool -list -cacerts -storepass changeit | grep -i company-internal-ca"
```

## Notes

- Replace the example subject `Company Internal CA` / alias `company-internal-ca`
  in the verification commands with your actual CA's common name or alias.
- Re-create the derived image whenever you pull a newer Portal or Keycloak base
  image, so the CA stays present and the public CA list stays current.
- Only add CA certificates you control and trust. Adding a CA means the container
  will trust **any** certificate that CA issues.
