feat: reorganise for cloud content

This commit is contained in:
Iain Learmonth 2026-02-22 19:21:02 +00:00
parent 7396dbc851
commit c7d058c599
24 changed files with 131 additions and 99 deletions

View file

@ -0,0 +1,5 @@
---
label: Operator Guide
position: 40
link:
type: "generated-index"

View file

@ -0,0 +1,103 @@
---
sidebar_position: 10
---
# Architecture
We begin the guide with a high-level overview of the architecture of the CDR Link deployment framework.
## Introduction
We deploy each CDR Link instance on a single
[Red Had Enterprise Linux](https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux)
9 (or compatible) host using
[rootless Podman](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md).
Each component of Link is a container instance with the containers managed via systemd using
[Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html).
Components communicate via isolated networks that are also configured via Quadlet, and make use of the
[slirp4netns](https://github.com/rootless-containers/slirp4netns) user-mode networking for unprivileged
network namespaces.
Both
[discretionary access controls](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/configuring_basic_system_settings/managing-file-system-permissions_configuring-basic-system-settings)
and
[SELinux](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html-single/using_selinux/index)
are used to prevent lateral movement between containers should a container be compromised, with particular
attention given to the messaging channels WhatsApp and Signal.
No container runs its application as the inside "root" user, which is an unprivileged user on the host.
The `/home` mount point on the host is encrypted using
[LUKS](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/managing_storage_devices/encrypting-block-devices-using-luks_managing-storage-devices)
with a per instance key to protect all user data at rest.
Further, we use separate partitions for critical audit logging to ensure that a resource exhaustion attack cannot
prevent later investigation, and automatically shut down instances where there is no space available for audit logging.
## Components
The following diagram shows the dependency relationships between the components of CDR Link.
If you use our [Ansible role](./deploy.md) for deployment then these will be automatically configured.
The Link stack containers are
[OCI](https://opencontainers.org/) compliant containers and you can run these with alternatives
such as [Docker Compose](https://docs.docker.com/compose/) however we would not be able to provide support for this
setup.
Our former Docker Compose deployment framework has been deprecated and we intend to migrate all partners to our new
rootless Podman setup for the improved reliability, performance, and security.
:::info
While the following diagram refers to `.service` units, these are the units generated by Podman Quadlet.
The service definitions are in `.container` units within the Quadlet directory at `$HOME/.config/containers/systemd`.
:::
```mermaid
flowchart TD
bridge-worker.service --> bridge-postgresql.service
bridge-worker.service -.-> bridge-whatsapp.service
bridge-worker.service -.-> signal-cli-rest-api.service
link.service --> bridge-postgresql.service
link.service --> bridge-worker.service
opensearch-dashboards.service --> zammad-opensearch.service
zammad-nginx.service --> zammad-railsserver.service
zammad-nginx.service --> link.service
zammad-storage.target{zammad-storage.target}
zammad-storage.target --> zammad-postgresql.service
zammad-storage.target --> zammad-redis.service
zammad-storage.target --> zammad-memcached.service
zammad-storage.target --> zammad-opensearch.service
zammad-railsserver.service -.-> zammad-init.service
zammad-init.service --> zammad-storage.target
zammad-railsserver.service --> zammad-storage.target
zammad-scheduler.service --> zammad-storage.target
zammad-websocket.service --> zammad-storage.target
link.target{link.target}
link.target --> opensearch-dashboards.service
link.target --> zammad-nginx.service
link.target --> zammad-scheduler.service
link.target --> zammad-websocket.service
```
All of the containers are built from the same [source repository](https://gitlab.com/digiresilience/link/link-stack)
and published in a public registry:
| Container Name | Image |
|-----------------------|-----------------------------------------------------------------------------------|
| bridge-postgresql | `registry.gitlab.com/digiresilience/link/link-stack/postgresql:latest` |
| bridge-whatsapp | `registry.gitlab.com/digiresilience/link/link-stack/bridge-whatsapp:latest` |
| bridge-worker | `registry.gitlab.com/digiresilience/link/link-stack/bridge-worker:latest` |
| link | `registry.gitlab.com/digiresilience/link/link-stack/link:latest` |
| opensearch-dashboards | `registry.gitlab.com/digiresilience/link/link-stack/opensearch-dashboards:latest` |
| signal-cli-rest-api | `registry.gitlab.com/digiresilience/link/link-stack/signal-cli-rest-api:latest` |
| zammad-init | `registry.gitlab.com/digiresilience/link/link-stack/zammad:latest` |
| zammad-memcached | `registry.gitlab.com/digiresilience/link/link-stack/memcached:latest` |
| zammad-nginx | `registry.gitlab.com/digiresilience/link/link-stack/zammad:latest` |
| zammad-opensearch | `registry.gitlab.com/digiresilience/link/link-stack/opensearch:latest` |
| zammad-postgresql | `registry.gitlab.com/digiresilience/link/link-stack/postgresql:latest` |
| zammad-railsserver | `registry.gitlab.com/digiresilience/link/link-stack/zammad:latest` |
| zammad-redis | `registry.gitlab.com/digiresilience/link/link-stack/redis:latest` |
| zammad-scheduler | `registry.gitlab.com/digiresilience/link/link-stack/zammad:latest` |
| zammad-websocket | `registry.gitlab.com/digiresilience/link/link-stack/zammad:latest` |

View file

@ -0,0 +1,70 @@
---
sidebar_position: 30
sidebar_label: Deployment Host
---
# Deployment Host Setup
Deployment takes place using [Ansible](https://docs.ansible.com/) which we will install in a
[venv](https://docs.python.org/3/library/venv.html) to allow for careful management of the versions of the software in
use.
For security, the deployment host must not run any network services listening on an external interface other than a
hardened SSH daemon if being used remotely. Ideally, the deployment host is operated locally via its terminal.
Begin by creating a directory for the deployment framework to operate from that should be owned by your unprivileged
user and group and have filesystem permissions of `0700`.
On systems with SELinux, a context of `user_home_t` should be appropriate.
This documentation will assume that you are working in the directory `$HOME/ops/`.
## Virtual Environment Setup
Begin by creating and activating a virtual environment:
```shell
cd $HOME/ops
python3 -m venv venv
source venv/bin/activate
```
Then install the dependencies we will require:
```shell
pip install ansible
pip install bitwarden-sdk # optional: only required for bitwarden secrets manager
```
## Install the Ansible collection and role dependencies
Create `$HOME/ops/requirements.yml`:
```yaml
---
collections:
- name: bitwarden.secrets # optional: only required for bitwarden secrets manager
- src: git+https://guardianproject.dev/sr2/ansible-collection-core.git
version: main # optional: only required for our baseline role
- src: git+https://guardianproject.dev/sr2/ansible-collection-apps.git
version: main # required: contains the CDR Link deployment role
roles:
- src: git+https://github.com/ansible-lockdown/RHEL9-CIS.git
version: "2.0.3" # optional: only required for our baseline role
```
Install the collections, and roles if required:
```shell
cd $HOME/ops
ansible-galaxy collection install -r requirements.yml
ansible-galaxy role install -r requirements.yml
```
## Create deployment data files and directories
Create the necessary directories that we will need in the next step:
```shell
cd $HOME/ops
mkdir {host,group}_vars
```

View file

@ -0,0 +1,64 @@
---
sidebar_position: 50
sidebar_label: Deploy CDR Link
---
# Deploy the CDR Link Stack
## Set up the Ansible inventory and host variables
Create `$HOME/ops/inventory`:
```ini
[cdr_link]
example.cdr.link
```
Create `$HOME/ops/group_vars/all.yml` if you use our baseline role and integrate with Identity Management:
```yaml
---
ipaserver_domain: CHANGEME
ipaserver_realm: CHANGEME
ipaserver_netbios_name: CHANGEME
```
Create `$HOME/ops/host_vars/example.cdr.link.yml`:
```yaml
---
baseline_second_disk_device: /dev/sdb # This is the device path for the data volume
baseline_home_luks_passphrase: CHANGEME
ipaclient_otp: CHANGEME # This is the OTP generated in the IdM step and is not sensitive after use
podman_link_podman_rootless_user: link_example # This is the user you created in the IdM step
podman_link_postgres_zammad_password: CHANGEME
podman_link_postgres_link_password: CHANGEME
podman_link_postgres_root_password: CHANGEME
podman_link_zammad_redis_password: CHANGEME
podman_link_opensearch_password: CHANGEME
podman_link_nextauth_secret: CHANGEME
```
:::warning
Do not store sensitive values in the `host_vars` file in plaintext, use lookup plugins to look up the secret values
using your secrets management solution of choice.
:::
The variables prefixed with `baseline_` and `ipaclient_` are only applicable if you are using our baseline role with
Identity Management integration.
If you manage your own hardening and use local users and groups you can omit these.
The user you reference in `podman_link_podman_rootless_user` must exist before continuing.
## Execute the Ansible playbook
If you are using our baseline role:
```shell
ansible-playbook -i inventory sr2.apps.link
```
If you manage your own hardening and identity management, use the `link` tag to only run the CDR Link stack deployment:
```shell
ansible-playbook -i inventory sr2.apps.link --tags link
```

View file

@ -0,0 +1,73 @@
---
sidebar_position: 40
sidebar_label: Identity Management
---
# Identity Management Setup
:::tip
If you are using an alternative Identity Management system or local user accounts, skip this page and go straight
to [Deploying with Ansible](./deploy.md).
:::
## Host Setup
It can be helpful to keep track of the following information in a text editor's buffer until deployment is complete.
None of these details are sensitive after the completion of the deployment.
```text
Hostname:
IPv4 Address:
IPv6 Address:
OTP:
```
### Add Host to DNS
1. Create an A record for the host
1. Create an AAAA record for the host
1. Create a null MX record for the host (e.g. `example.cdr.link IN MX 0 .`)
### Add Host to Identity Management
1. Begin by logging in to the Identity Management server with your privileged identity
1. Open the **Identity** tab, and select the **Hosts** subtab
1. Click **Add** at the top of the hosts list
1. Enter the name of the new host, e.g. `example.cdr.link`
1. The IP address will be automatically resolved from DNS, you can leave this blank but may need to allow a moment for
the authoritative DNS servers to update
1. Activate the **Generate OTP** checkbox
1. Click **Add** to add the new host
1. Save the generated OTP for later
![Screenshot of the Add Host Wizard in Identity Management](/img/host-add.png)
## User Setup
### Create the Service User
This is the user on the host that will run the Podman containers.
1. Open the **Identity** tab, and select the **Users** subtab
1. Click **Add** at the top of the users list
1. Enter a **Username**, we prefix all our Link service users with `link_` for easy identification
1. Enter a **First Name** and **Last Name**, these values do not matter but the LDAP schema requires them
1. Do not enter a **New Password** as this user will never need to authenticate with a password
1. Click **Add**
![Screenshot of the Add User Wizard in Identity Management](/img/user-add.png)
### Generate subordinate IDs for the user
1. Open the **Identity** tab, and expand the **Subordinate IDs** subtab
1. Choose the **Subordinate IDs** option from the drop-down menu
1. Click the **Add** button in the upper-right corner of the interface
1. In the **Add subid** window, select the user you have just created as the **Owner**
1. Click **Add**
The range is automatically generated and managed by Identity Management.
:::tip
If you are not using our baseline Ansible role, ensure that the `with-subid` feature of the `sssd` authselect profile is
enabled to allow hosts to look up subids in LDAP.
:::

View file

@ -0,0 +1,105 @@
---
sidebar_position: 20
---
# Requirements
If you are looking to self-host CDR Link, this page details what is required to run a secure and reliable instance.
The following requirements assume that your deployment framework will be the same as ours.
You may need to adjust the requirements if your framework differs materially, and while we can offer limited support for
this we would not be able to assist with in-depth troubleshooting.
## Deployment Host
:::danger
A compromise of this host effectively compromises the entire stack.
:::
* Appropriately hardened and vendor supported Linux operating system with the latest security updates applied
* SSH key backed by hardware security module and requiring unlock (e.g. [YubiKey](https://www.yubico.com/)) to be used
for login to the instance host
* [Python](https://www.python.org/) 3.11+ (the `venv` module is included in Python since 3.3 so this is not a separate requirement)
* [Git](https://git-scm.com/)
* [Ansible](https://docs.ansible.com/)-compatible secrets management (e.g.
[sops](https://getsops.io/) or
[Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/))
## CDR Link Instance Host
It is **strongly recommended** that this host is dedicated to running the CDR Link software only.
* A [Red Hat Enterprise Linux](https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux)
9 (or compatible) host with the latest security updates and appropriate hardening applied
* At least 4 (v)CPUs, 15GB RAM, and 75GB disk space
* Separate partitions for `/var`, `/var/log`, `/var/log/audit`, and `/home`
* `/home` must use the
[XFS](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/managing_file_systems/getting-started-with-xfs_managing-file-systems)
file system (to apply quotas)
* A global scope IP address assigned to one of the network interfaces
* FirewallD is installed and enabled with only the `ssh` service enabled
* Our deployment script will add the `http` and `https` services
* An unprivileged user created with 65536
[subuids and subgids](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/building_running_and_managing_containers/assembly_starting-with-containers_building-running-and-managing-containers#proc_setting-up-rootless-containers_assembly_starting-with-containers)
assigned that will be used to run the Link stack
* An unprivileged user with sudo permission, and the SSH public key from the deployment host installed as an authorised
key
In our deployments, we would perform the hardening configuration using our
[baseline Ansible role](https://guardianproject.dev/sr2/ansible-collection-core/src/branch/main/roles/baseline),
however this role is not intended for general use and so may not fit your requirements.
We manage our users, groups, and subordinate IDs using
[Identity Management](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/installing_identity_management/index)
but a locally created user can also be used.
:::warning
Depending on your configuration it is also possible to use an LDAP user with locally defined subuids and subgids however
be aware that this may lead to conflicts between hosts, and complications if file system snapshots or backups are
available across hosts.
:::
## CDR Link Handset (Optional)
The handset is only required if you wish to use the Signal or WhatsApp channels with CDR Link.
It is **strongly recommended** that this handset is dedicated to running the CDR Link related apps only and is stored in
a secure facility.
Consider that the device will require to have its cellular connection available and so it would not be advised to store
the handset, for example, in a metallic container.
:::tip
Many of the handset related support requests we receive are due to partners using the handset directly rather than
allowing CDR Link to manage the channels leading to the system losing synchronisation and then failing to recover.
:::
The handset has the ability to send and receive messages via the helpdesk channels and so should not be frequently moved
between locations to reduce the risk of theft or other loss.
Cheaper handsets often do not receive security updates for Android promptly after their release as there are multiple
steps in the supply chain before an update can be distributed. Ensure that you purchase your handset from a reputable
manufacturer that provides a guaranteed period of security updates and has a track record of timely distribution of
those updates.
* At least Android 14, preferably at least 15 for new deployments, with latest system updates installed
* Google Play Services are required for Signal and WhatsApp
* Cellular plan that can receive SMS and with enough data to allow for system updates
* SMS required to allow activation of the WhatsApp and Signal accounts
* We have seen lower rates of accounts being blocked (automatically flagged as spam or fraud) when using cellular data
plans as opposed to WiFi
* At least 3GB RAM and 16GB storage
* [Mobile Device Management](https://www.android.com/intl/en_uk/enterprise/management/) for configuration, automatic
updates and remote wipe capability
* Appropriate hardening, for example:
* Ensure that a complex screen lock is used
* Ensure that device encryption is enabled
* Ensure that developer options are disabled
* Ensure screen timeout is set
* Ensure lock screen does not show notifications
* Disable Bluetooth and WiFi
* 7-day timer to manage charging to avoid failure of the internal battery
:::danger
If you leave the phone permanently connected to the charger, the internal battery of the handset will fail and may pose
a fire hazard.
You should consider any charging of lithium-ion batteries in your local site risk assessments.
:::