# Secure Setup Guidelines

This page describes the recommended way to operate UDiTH Portal with a small
attack surface. It is a hardening guide, not an installation guide. For the
installation steps, see [Setup Portal Linux](/UDiTH%20Portal/Setup%20Portal%20Linux)
and [Requirements](/UDiTH%20Portal/Requirements).

## Summary

| Topic            | Recommendation                                              |
| ---------------- | ----------------------------------------------------------- |
| Platform         | Linux with Docker (or Podman)                                |
| Image variant    | `ubuntu-chiseled`                                            |
| Isolation        | `cap_drop: ALL`, `no-new-privileges`, AppArmor profile        |
| Transport        | HTTPS only, legacy TLS protocols switched off                 |
| External access  | Reverse proxy, Caddy or the product of your choice           |
| Secrets          | Azure Key Vault, or Docker secrets                           |
| Maintenance      | Keep the host and all applications up to date                |

## Use Linux

Run Portal on Linux in containers. A container gives you process, file system
and network isolation that the Windows installation on IIS cannot provide.

- Use a supported LTS distribution.
- Install only the packages that the container runtime needs.
- Keep the Docker daemon and the container runtime current.

## Use the `ubuntu-chiseled` Image

The `quay.io/caxperts/portal` image is published in several variants. For a
secure setup, use `ubuntu-chiseled`.

- The chiseled image is a minimal, distroless-style image. It contains **no
  shell and no package manager**.
- Without a shell, an attacker who reaches the container has almost no tools
  available.
- The image contains fewer packages, so it has fewer known vulnerabilities.
- The container runs as the unprivileged user with uid `1654`, not as `root`.

The other variants (`ubuntu`, `alpine`, `noble`) run as `root` inside the
container. Use them only if you must run commands in the container, for example
for support or diagnostics.

Prepare the bind-mounted volumes for uid `1654`:

```bash
mkdir -p /srv/udith/storage /srv/udith/import /srv/udith/upload
chown -R 1654:1654 /srv/udith/storage /srv/udith/import /srv/udith/upload
chmod -R u+rwX /srv/udith/storage /srv/udith/import /srv/udith/upload
```

For details on the variants, the uids and the volume permissions, see
[Image Variants and Volume Permissions](/UDiTH%20Portal/Setup%20Portal%20Linux#image-variants-and-volume-permissions).
To add your own CA certificates to the chiseled image, see
[Adding Custom CA Certificates](/UDiTH%20Portal/Adding%20Custom%20CA%20Certificates).

### Additional Container Settings

Add the following settings to the Portal service in your `docker-compose.yml`:

```yml
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
```

Notes:

- `read_only` requires that all writable paths are volumes or `tmpfs`. Test this
  setting before you use it in production.
- Do not publish the container port to the host. Let the reverse proxy reach the
  container over an internal Docker network.

### AppArmor Profile

CAXperts provides an AppArmor profile for the Portal container. The profile
restricts the container to the files, directories and network operations that
Portal actually needs.

> ⚠️ **Important**: The profile is only valid for the `ubuntu-chiseled` variant.
> The other variants have a different file system layout and a different user,
> so the profile does not match them.

Save the profile as `/etc/apparmor.d/docker-portal` on the host:

```
# Last Modified: Tue Jul  7 17:08:13 2026
abi <abi/4.0>,

include <tunables/global>

profile docker-portal flags=(attach_disconnected, complain, mediate_deleted) {
  include <abstractions/base>
  include <abstractions/ssl_certs>
  include <abstractions/user-tmp>

  network inet dgram,
  network inet stream,
  network inet6 dgram,
  network inet6 stream,
  network netlink raw,

  deny mount,
  deny umount,

  mqueue getattr type=posix,

  deny ptrace trace,

  deny /proc/kcore r,
  deny /proc/sys/kernel/** wlkx,
  deny /sys/firmware/** rwlkx,
  deny /sys/fs/cgroup/** wlkx,
  deny /sys/kernel/security/** rwlkx,

  /app/Portal/{,**} rk,
  /etc/resolv.conf rk,
  /etc/{host.conf,hosts,passwd} r,
  /proc/*/net/{ipv6_route,route} rk,
  /proc/sys/net/ipv4/conf/*/forwarding rk,
  /proc/{cpuinfo,meminfo} rk,
  /run/secrets/* rk,
  /sys/devices/system/cpu/** r,
  /sys/fs/cgroup/** r,
  /usr/share/dotnet/** mrk,
  /{import,storage,upload}/{,**} rwk,
  /{usr/,}lib{,32,64}/** rk,
  owner /home/app/.aspnet/{,**} rwk,
  owner /proc/*/task/ r,
  owner /proc/*/task/*/comm rw,
  owner /proc/*/task/*/stat rk,
  owner /proc/*/{cgroup,cmdline,mountinfo,mounts,stat,status} rk,

}
```

Load the profile and confirm that it is active:

```bash
sudo apparmor_parser -r /etc/apparmor.d/docker-portal
sudo aa-status | grep docker-portal
```

Assign the profile to the Portal service in your `docker-compose.yml`:

```yml
    security_opt:
      - no-new-privileges:true
      - apparmor=docker-portal
```

Recreate the container to apply the profile.

The profile is delivered with the `complain` flag. In this mode AppArmor allows
every operation and only logs the violations. Use this mode first, and check the
log for denials:

```bash
sudo journalctl -k | grep -i apparmor
```

When no denial appears during normal operation, switch the profile to enforce
mode. Remove `complain` from the `flags=(...)` list, or run:

```bash
sudo aa-enforce /etc/apparmor.d/docker-portal
```

Notes:

- The profile must be loaded on the **host**. The container itself contains no
  AppArmor configuration.
- The paths `/import`, `/storage` and `/upload` are the only writable mounts.
  If you mount additional volumes, add them to the profile.
- The profile does not replace the other container settings. Use it together
  with `cap_drop`, `no-new-privileges` and the non-root user.

## Use a Reverse Proxy

Do not expose Portal or Keycloak directly to the internet. Put a reverse proxy in front of
it. The proxy terminates TLS, provides the certificate and hides the
application.

We recommend [Caddy](https://caddyserver.com/), because it requests and renews
certificates automatically and uses secure TLS defaults. Any other reverse proxy
is also supported. Use the product that your organisation already operates, for
example NGINX, HAProxy, Traefik or a hardware appliance.

Requirements for the reverse proxy:

- Terminate HTTPS with a trusted certificate for the Portal domain.
- Forward `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host`.
- Allow WebSocket connections.
- Do not limit the upload size below the size of the models that your users
  upload.

### Sample `Caddyfile`

```caddyfile
portal.example.com {
	reverse_proxy portal_demo:8080
}

keycloak.example.com {
	reverse_proxy keycloak:8080
}
```

Caddy sets the `X-Forwarded-*` headers and handles WebSocket upgrades without
extra configuration. Replace the host names with the service names of your
Compose project, and confirm the container port of your Portal version before
you use this file.

Keycloak needs `KC_PROXY_HEADERS: xforwarded` and `KC_HOSTNAME` for operation
behind a proxy. See the Keycloak sample in
[Setup Portal Linux](/UDiTH%20Portal/Setup%20Portal%20Linux).

## TLS and Certificates

- Use a trusted certificate that meets current security standards. See the
  [Common requirements](/UDiTH%20Portal/Requirements#common-requirements)
  section.
- Switch off outdated or insecure legacy TLS protocols on every web server. This
  applies to the reverse proxy, to IIS on Windows and to Keycloak.
- Keep `IgnoreCertificateErrors` at the default value `false`. This setting
  accepts invalid TLS certificates for outgoing connections and removes the
  protection against a man-in-the-middle attack.
- Terminate TLS in the reverse proxy. Terminate TLS directly in Kestrel only if
  a fully encrypted connection to the container is required. See the
  [Portal Configuration](/UDiTH%20Portal/Portal%20Configuration) page.
- If your certificate comes from a company-internal Certificate Authority, the
  CA certificate must be trusted inside the container. See
  [Adding Custom CA Certificates](/UDiTH%20Portal/Adding%20Custom%20CA%20Certificates).

## Keycloak

Keycloak holds the identities of all users. Protect it at least as well as
Portal.

- Keep Keycloak up to date with the latest security fixes.
- Allow incoming traffic only on the port on which Keycloak runs.
- Behind a reverse proxy, set `KC_PROXY_HEADERS: xforwarded` and `KC_HOSTNAME`.
- Use a separate database and a separate database user for Keycloak.

## Windows Installations

The Linux container setup described above is the recommended deployment. If you
operate Portal on Windows with IIS, the following points apply.

- Keep Windows and IIS up to date with the latest security fixes.
- Switch off outdated or insecure legacy TLS protocols in the operating system
  and in IIS.
- HTTPS is required. A setup over HTTP is not supported.
- Protect `Settings/sharedsettings.json` in the installation directory. It holds
  the secrets. Restrict the NTFS permissions to the application pool identity
  and to the administrators.
- Read the secrets from an Azure Key Vault instead, as described above.
- Run the application pool with a dedicated service account that has the minimum
  permissions.

## Keep the Host and the Applications Up to Date

Updates are the most effective protection. Plan a regular maintenance window.

- Install the security updates of the operating system. Enable unattended
  security updates if your change process permits it.
- Pull the current Portal, CAXturn, Keycloak and MSSQL images and recreate the
  containers.
- Pin an explicit image tag instead of `latest`, so that you know which version
  runs. Change the tag when you update.
- Read [Breaking Changes](/UDiTH%20Portal/Breaking%20Changes) before each Portal
  update.
- Keep the render servers and the client workstations up to date.

## Load Secrets from Azure Key Vault

Do not keep secrets in the `docker-compose.yml` file, in an `.env` file or in
the environment of the container. Portal can read any setting from an Azure Key
Vault instead.

A secret name maps to a setting name. Replace each colon in the setting name
with a double dash. For example, the secret `ConnectionStrings--AdminConnection`
provides the setting `ConnectionStrings:AdminConnection`. Portal queries the
vault only when a vault URI is configured.

```yml
    environment:
      - AzureKeyVault__VaultUri=https://my-vault.vault.azure.net/
      - AzureKeyVault__Prefix=prod-
```

Recommendations:

- Use the ambient Azure identity, for example a managed identity. Leave
  `AzureKeyVault:TenantID`, `AzureKeyVault:ClientID` and
  `AzureKeyVault:ClientSecret` unset. No credential is then stored on the host.
- If a managed identity is not available, prefer a client certificate
  (`AzureKeyVault:ClientCertificatePath`) over a client secret.
- Give the identity only the **get** and **list** permissions for secrets.
- Use `AzureKeyVault:Prefix` to serve several environments from one vault.
- Enable soft delete, purge protection and the audit log of the vault.
- Rotate the secrets in the vault. Restart the container to apply new values.

For the full list of the Key Vault settings, see the **Azure Key Vault** section
on the [Portal Configuration](/UDiTH%20Portal/Portal%20Configuration) page.

## Further Recommendations

- If you cannot use a Key Vault, use Docker secrets. Any variable can be read
  from a file. Append the `_file` suffix to the variable name, for example
  `KeycloakAdmin__Password_file=/run/secrets/keycloak_password`.
- Open only the necessary ports in the firewall. See the
  [Ports](/UDiTH%20Portal/Requirements#ports) section.
- Do not publish the MSSQL port `1433` to the internet.
- Use a separate database user with the minimum permissions for Portal and for
  Keycloak.
- Enable the logs and metrics endpoints and monitor them. See
  [Observability](/UDiTH%20Portal/Observability).
- Restrict the permissions of the users in Portal. See
  [Permission Management](/UDiTH%20Portal/Permission%20Management).
