mirror of
https://github.com/kerberos-io/agent.git
synced 2026-08-23 15:08:32 +00:00
Merge pull request #316 from kerberos-io/feature/srtsp-support
feature/srtsp-support
This commit is contained in:
639
README-RTSPS-TLS.md
Normal file
639
README-RTSPS-TLS.md
Normal file
@@ -0,0 +1,639 @@
|
||||
# RTSPS and TLS certificates
|
||||
|
||||
This guide explains how Kerberos Agent connects to an IP camera over RTSPS,
|
||||
how to issue a camera certificate with a private CA, and how to validate the
|
||||
complete trust path. It also explains why some apparently corrupted trust
|
||||
bundles can still allow a connection.
|
||||
|
||||
The camera-specific steps were verified with a Bosch FLEXIDOME micro 3100i.
|
||||
Other Bosch firmware versions may use different labels or ports.
|
||||
|
||||
The commands were tested with Smallstep CLI `0.30.6` and OpenSSL `3.5.6` on
|
||||
Debian. Check `step certificate sign --help` when using an older Smallstep CLI.
|
||||
The OpenSSL isolation flags `-no-CApath` and `-no-CAstore` require a version that
|
||||
lists them in `openssl s_client -help`.
|
||||
|
||||
## Tested configuration
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| Camera | Bosch FLEXIDOME micro 3100i |
|
||||
| Example camera address | `10.0.30.11` |
|
||||
| RTSPS port | `9554` |
|
||||
| Main stream | `rtsps://<user>:<password>@10.0.30.11:9554/?inst=1` |
|
||||
| Sub stream | `rtsps://<user>:<password>@10.0.30.11:9554/?inst=2` |
|
||||
| Certificate SAN | `IP Address:10.0.30.11` |
|
||||
| Bosch certificate usage | `HTTPS` |
|
||||
| Agent trust input | Issuing intermediate plus root CA |
|
||||
|
||||
Replace the example address and certificate names throughout this guide. Keep
|
||||
camera credentials out of source control and percent-encode reserved URL
|
||||
characters in usernames and passwords.
|
||||
|
||||
## Mental model
|
||||
|
||||
### RTSPS, SRTSP, TLS, and SRTP
|
||||
|
||||
- The standard URL scheme is `rtsps://`. Do not use `srtsp://`.
|
||||
- Bosch interfaces and documentation may use SRTSP or Secure RTSP as product
|
||||
terminology.
|
||||
- RTSPS carries the RTSP control connection over TLS. With gortsplib, media is
|
||||
normally interleaved over the same TCP/TLS connection for this camera.
|
||||
- SRTP is a separate media protection mechanism and is negotiated only when the
|
||||
camera advertises an appropriate secure RTP profile.
|
||||
|
||||
Encryption alone does not prove which camera the Agent reached. Verified TLS
|
||||
also checks that:
|
||||
|
||||
1. The camera certificate is signed by a trusted authority.
|
||||
2. The certificate is valid at the current time.
|
||||
3. The URL host matches a certificate Subject Alternative Name (SAN).
|
||||
|
||||
Modern Go verification uses SANs for identity. A Common Name alone is not
|
||||
sufficient. Connecting to `10.0.30.11` requires an IP SAN with that exact value,
|
||||
not `DNS:10.0.30.11` and not only a device-name DNS SAN.
|
||||
|
||||
### Agent behavior
|
||||
|
||||
Kerberos Agent uses gortsplib for RTSP and RTSPS. With the normal configuration,
|
||||
gortsplib receives a nil custom TLS configuration and Go performs standard
|
||||
certificate and hostname verification with the process trust pool.
|
||||
|
||||
`AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=true` is an explicit escape hatch that
|
||||
sets `InsecureSkipVerify` for camera clients. It should be false in a verified
|
||||
deployment.
|
||||
|
||||
## Communication and certificate flow
|
||||
|
||||
The certificate is used during the TLS handshake, before the first RTSP command
|
||||
is exchanged. It is not attached to `DESCRIBE`, `SETUP`, or `PLAY`, and the CA
|
||||
trust bundle is never sent to the camera.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Agent as Kerberos Agent
|
||||
participant Trust as Go trust pool
|
||||
participant Camera as Camera RTSPS :9554
|
||||
|
||||
Agent->>Trust: Load trusted CAs from SSL_CERT_FILE and CA directories
|
||||
Agent->>Camera: Open TCP connection
|
||||
Agent->>Camera: Send TLS ClientHello
|
||||
Camera-->>Agent: Send TLS ServerHello and camera certificate
|
||||
Agent->>Trust: Verify chain, validity, serverAuth, and URL host against SAN
|
||||
Trust-->>Agent: Accept or reject the camera identity
|
||||
Agent->>Camera: Complete TLS handshake
|
||||
Note over Agent,Camera: All following traffic is encrypted by TLS
|
||||
Agent->>Camera: DESCRIBE with RTSP authentication
|
||||
Camera-->>Agent: Return SDP and available media tracks
|
||||
Agent->>Camera: SETUP selected video and audio tracks over TCP
|
||||
Agent->>Camera: PLAY
|
||||
Camera-->>Agent: Send interleaved RTP and RTCP media over TLS
|
||||
```
|
||||
|
||||
The files and keys have distinct roles:
|
||||
|
||||
| Material | Location | Purpose | Sent over the connection |
|
||||
| --- | --- | --- | --- |
|
||||
| Camera leaf certificate | Camera | Identifies the camera and binds its public key to its SAN | Yes, by the camera during the TLS handshake |
|
||||
| Camera private key | Camera | Proves that the camera owns the presented certificate | No |
|
||||
| Intermediate and root CA PEM bundle | Agent | Lets Go build and trust the camera certificate chain | No |
|
||||
| RTSP username and password | Agent configuration or URL | Authenticates the Agent to the RTSP service after TLS succeeds | An authentication response is sent inside TLS; its form depends on the RTSP authentication method |
|
||||
|
||||
For an `rtsps://` URL, the Agent parses the URL and gives gortsplib the host and
|
||||
TLS settings. gortsplib opens the TCP connection and starts TLS. Go compares the
|
||||
certificate presented by the camera with the local trust pool, checks its
|
||||
validity period and server usage, and matches the URL hostname or IP address to
|
||||
the certificate SAN. Only a successful handshake creates the encrypted channel
|
||||
needed for the RTSP exchange.
|
||||
|
||||
The Agent then sends `DESCRIBE`, selects the advertised video and audio tracks,
|
||||
sends `SETUP`, and starts delivery with `PLAY`. For the tested camera, gortsplib
|
||||
uses interleaved TCP, so the RTSP control messages and RTP/RTCP media remain
|
||||
inside the same encrypted TLS connection. Main stream, sub stream, and enabled
|
||||
audio backchannel clients each establish and verify their own connection.
|
||||
|
||||
If certificate verification fails, the TLS handshake does not complete and no
|
||||
usable RTSP session is established. Setting
|
||||
`AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=true` keeps traffic encrypted but skips
|
||||
certificate-chain and hostname verification, so an attacker could impersonate
|
||||
the camera. It is not equivalent to trusting the camera certificate.
|
||||
|
||||
## Decide the certificate identity first
|
||||
|
||||
Choose the stable name used in every Agent URL before creating the certificate:
|
||||
|
||||
- For an IP URL, add that address as an IP SAN.
|
||||
- For a DNS URL, add the exact hostname as a DNS SAN.
|
||||
- Add both when clients legitimately use both forms.
|
||||
|
||||
A certificate stops matching if the camera IP changes. Use a static address,
|
||||
DHCP reservation, or stable DNS name.
|
||||
|
||||
## Configure RTSPS in the Bosch UI
|
||||
|
||||
1. Sign in to the camera as an administrator.
|
||||
2. Open **Configuration**.
|
||||
3. Open **Network > Network Services**.
|
||||
4. Enable **RTSPS**.
|
||||
5. Confirm port `9554`, or record the configured alternative.
|
||||
6. Click **Set**.
|
||||
|
||||
RTSP on port `554` and RTSPS on port `9554` are separate services. Enabling
|
||||
RTSPS does not make an `rtsp://` URL secure.
|
||||
|
||||
## Generate the private key and CSR on the camera
|
||||
|
||||
Keeping the TLS private key on the camera avoids exporting it to an operator
|
||||
workstation or deployment system.
|
||||
|
||||
1. Open **Service > Certificates**.
|
||||
2. Click **Add**.
|
||||
3. Select **Generate signing request**.
|
||||
4. Select `RSA 2048bit` or the stronger option supported by all clients.
|
||||
5. Enter a unique file name, such as `agent-rtsps`.
|
||||
6. Enter a descriptive Common Name and any required organization fields.
|
||||
7. Click **Generate**.
|
||||
8. Download the resulting CSR from the certificate table.
|
||||
|
||||
On the tested firmware, this form contains no SAN field. The downloaded CSR
|
||||
therefore has no IP SAN. The CA must add the SAN while signing.
|
||||
|
||||
Inspect the CSR before signing:
|
||||
|
||||
```bash
|
||||
openssl req -in camera.csr.pem -noout -verify -subject
|
||||
openssl req -in camera.csr.pem -noout -text
|
||||
```
|
||||
|
||||
The first command must report `Certificate request self-signature verify OK`.
|
||||
An absent `Subject Alternative Name` section is expected for this firmware.
|
||||
|
||||
## Prepare Smallstep
|
||||
|
||||
Use an existing organizational CA when one is available. Creating a new CA
|
||||
creates a new long-lived trust domain that must be distributed, protected,
|
||||
backed up, and eventually rotated.
|
||||
|
||||
### Install the CLI on Debian amd64
|
||||
|
||||
```bash
|
||||
curl -fsSL \
|
||||
https://dl.smallstep.com/cli/docs-ca-install/latest/step-cli_amd64.deb \
|
||||
-o /tmp/step-cli_amd64.deb
|
||||
sudo dpkg -i /tmp/step-cli_amd64.deb
|
||||
rm /tmp/step-cli_amd64.deb
|
||||
step version
|
||||
```
|
||||
|
||||
Use the official package matching the host architecture on other systems.
|
||||
|
||||
### Create a dedicated offline CA
|
||||
|
||||
Skip this section when using an existing CA.
|
||||
|
||||
```bash
|
||||
umask 077
|
||||
mkdir -p "$HOME/.step/secrets" "$HOME/.step/camera"
|
||||
|
||||
openssl rand -base64 48 > "$HOME/.step/secrets/camera_ca_password"
|
||||
chmod 600 "$HOME/.step/secrets/camera_ca_password"
|
||||
|
||||
step ca init \
|
||||
--pki \
|
||||
--name "UUG Camera CA" \
|
||||
--password-file "$HOME/.step/secrets/camera_ca_password"
|
||||
```
|
||||
|
||||
This produces:
|
||||
|
||||
```text
|
||||
$HOME/.step/certs/root_ca.crt
|
||||
$HOME/.step/certs/intermediate_ca.crt
|
||||
$HOME/.step/secrets/root_ca_key
|
||||
$HOME/.step/secrets/intermediate_ca_key
|
||||
$HOME/.step/secrets/camera_ca_password
|
||||
```
|
||||
|
||||
The files under `secrets/` are sensitive. Keep them mode `600`, never commit
|
||||
them, and back them up to encrypted persistent storage. A devcontainer can be
|
||||
rebuilt or deleted; it is not sufficient as the only CA backup.
|
||||
|
||||
## Add the SAN while signing
|
||||
|
||||
Copy the camera CSR into a protected working directory:
|
||||
|
||||
```bash
|
||||
cp camera.csr.pem "$HOME/.step/camera/camera.csr.pem"
|
||||
```
|
||||
|
||||
Create `$HOME/.step/camera/bosch-rtsps.tpl`:
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": {
|
||||
"commonName": {{ toJson .Insecure.CR.Subject.CommonName }}
|
||||
},
|
||||
"ipAddresses": ["10.0.30.11"],
|
||||
"keyUsage": ["keyEncipherment", "digitalSignature"],
|
||||
"extKeyUsage": ["serverAuth", "clientAuth"]
|
||||
}
|
||||
```
|
||||
|
||||
The template preserves the camera CSR public key, sets the IP identity, and
|
||||
creates a TLS leaf rather than a CA certificate.
|
||||
|
||||
Sign it with a validity period that ends before the intermediate CA expires.
|
||||
A one-year leaf is preferable to a ten-year leaf when automated renewal is
|
||||
available:
|
||||
|
||||
```bash
|
||||
step certificate sign \
|
||||
--template "$HOME/.step/camera/bosch-rtsps.tpl" \
|
||||
--bundle \
|
||||
--not-after 8760h \
|
||||
--password-file "$HOME/.step/secrets/camera_ca_password" \
|
||||
"$HOME/.step/camera/camera.csr.pem" \
|
||||
"$HOME/.step/certs/intermediate_ca.crt" \
|
||||
"$HOME/.step/secrets/intermediate_ca_key" \
|
||||
> "$HOME/.step/camera/bosch-rtsps-chain.pem"
|
||||
```
|
||||
|
||||
For an online `step-ca`, do not assume `step ca sign` accepts a `--san` flag. It
|
||||
does not. Authorize SANs in the one-time token or configure a provisioner
|
||||
template that produces the required SANs.
|
||||
|
||||
## Validate before upload
|
||||
|
||||
Inspect the leaf certificate, which is the first PEM block in the chain file:
|
||||
|
||||
```bash
|
||||
openssl x509 \
|
||||
-in "$HOME/.step/camera/bosch-rtsps-chain.pem" \
|
||||
-noout -subject -issuer -dates -ext subjectAltName -ext extendedKeyUsage
|
||||
```
|
||||
|
||||
Confirm the SAN separately because some OpenSSL versions display only the last
|
||||
requested extension:
|
||||
|
||||
```bash
|
||||
openssl x509 \
|
||||
-in "$HOME/.step/camera/bosch-rtsps-chain.pem" \
|
||||
-noout -ext subjectAltName
|
||||
```
|
||||
|
||||
Verify the path and IP identity:
|
||||
|
||||
```bash
|
||||
openssl verify \
|
||||
-CAfile "$HOME/.step/certs/root_ca.crt" \
|
||||
-untrusted "$HOME/.step/certs/intermediate_ca.crt" \
|
||||
-verify_ip 10.0.30.11 \
|
||||
"$HOME/.step/camera/bosch-rtsps-chain.pem"
|
||||
```
|
||||
|
||||
Confirm that the signed leaf uses the exact public key from the camera CSR:
|
||||
|
||||
```bash
|
||||
csr_key=$(
|
||||
openssl req -in "$HOME/.step/camera/camera.csr.pem" -pubkey -noout |
|
||||
openssl pkey -pubin -outform DER 2>/dev/null |
|
||||
sha256sum | cut -d' ' -f1
|
||||
)
|
||||
|
||||
cert_key=$(
|
||||
openssl x509 -in "$HOME/.step/camera/bosch-rtsps-chain.pem" -pubkey -noout |
|
||||
openssl pkey -pubin -outform DER 2>/dev/null |
|
||||
sha256sum | cut -d' ' -f1
|
||||
)
|
||||
|
||||
test "$csr_key" = "$cert_key"
|
||||
```
|
||||
|
||||
Do not upload a certificate when any of these checks fail.
|
||||
|
||||
## Upload and assign the certificate
|
||||
|
||||
1. Return to **Service > Certificates**.
|
||||
2. Click **Add > Upload certificate**.
|
||||
3. Select the leaf-plus-intermediate PEM chain.
|
||||
4. Click **Upload** and wait for `100%`.
|
||||
5. Confirm that the former CSR row is now a `Certificate`.
|
||||
6. Confirm that the key icon is present. It proves that the camera associated
|
||||
the certificate with its retained private key.
|
||||
7. Open the new certificate's **Usage** selector.
|
||||
8. Select only **HTTPS**.
|
||||
9. Leave **CBS client** assigned to the original Bosch `DeviceCertificate`.
|
||||
10. Click **Set** and wait for the table to reload.
|
||||
|
||||
On the tested firmware, there is no separate SRTSP usage. RTSPS presents the
|
||||
certificate assigned to HTTPS. Reassigning HTTPS therefore changes both the
|
||||
web interface and RTSPS certificate.
|
||||
|
||||
After saving, the expected split is:
|
||||
|
||||
| Certificate | Usage |
|
||||
| --- | --- |
|
||||
| Private-CA camera certificate | `HTTPS` |
|
||||
| Bosch `DeviceCertificate` | `CBS client` |
|
||||
|
||||
The browser may warn about the new HTTPS certificate until the private root CA
|
||||
is trusted by the workstation.
|
||||
|
||||
## Account for the Bosch chain behavior
|
||||
|
||||
The tested firmware served only the leaf certificate on ports `443` and `9554`,
|
||||
even when the uploaded file contained the leaf and intermediate. Uploading the
|
||||
intermediate separately as a trusted camera certificate did not change the
|
||||
served chain.
|
||||
|
||||
Confirm the behavior:
|
||||
|
||||
```bash
|
||||
openssl s_client \
|
||||
-connect 10.0.30.11:9554 \
|
||||
-showcerts </dev/null 2>/dev/null |
|
||||
grep -c '^-----BEGIN CERTIFICATE-----$'
|
||||
```
|
||||
|
||||
A result of `1` means the client must already have the issuing intermediate.
|
||||
Create a portable trust bundle containing the intermediate and root:
|
||||
|
||||
```bash
|
||||
step certificate bundle \
|
||||
"$HOME/.step/certs/intermediate_ca.crt" \
|
||||
"$HOME/.step/certs/root_ca.crt" \
|
||||
"$HOME/.step/camera/uug-camera-trust-bundle.pem"
|
||||
|
||||
chmod 644 "$HOME/.step/camera/uug-camera-trust-bundle.pem"
|
||||
```
|
||||
|
||||
The trust bundle is public material. The CA private keys and password are not.
|
||||
|
||||
## Configure Kerberos Agent
|
||||
|
||||
For a process running directly in the same environment:
|
||||
|
||||
```dotenv
|
||||
AGENT_CAPTURE_IPCAMERA_RTSP="rtsps://<user>:<password>@10.0.30.11:9554/?inst=1"
|
||||
AGENT_CAPTURE_IPCAMERA_SUB_RTSP="rtsps://<user>:<password>@10.0.30.11:9554/?inst=2"
|
||||
AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=false
|
||||
SSL_CERT_FILE=/home/agent/data/config/uug-camera-trust-bundle.pem
|
||||
```
|
||||
|
||||
For a container, mount the public bundle read-only at the exact path visible
|
||||
inside the container. The Agent image creates `/home/agent/data/config` and
|
||||
includes Debian's `ca-certificates` package:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v /secure/config/uug-camera-trust-bundle.pem:/home/agent/data/config/uug-camera-trust-bundle.pem:ro \
|
||||
-e SSL_CERT_FILE=/home/agent/data/config/uug-camera-trust-bundle.pem \
|
||||
-e AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=false \
|
||||
-e 'AGENT_CAPTURE_IPCAMERA_RTSP=rtsps://<user>:<password>@10.0.30.11:9554/?inst=1' \
|
||||
-e 'AGENT_CAPTURE_IPCAMERA_SUB_RTSP=rtsps://<user>:<password>@10.0.30.11:9554/?inst=2' \
|
||||
kerberos/agent:latest
|
||||
```
|
||||
|
||||
Restart the Agent after changing trust files. Go can cache the process system
|
||||
certificate pool after its first use, so editing a file does not guarantee that
|
||||
an already-running process reloads it.
|
||||
|
||||
The default production mode retains the image's normal public roots in addition
|
||||
to the private camera CA. For a deliberately private-CA-only deployment, mount
|
||||
an empty directory and set `SSL_CERT_DIR` to its path:
|
||||
|
||||
```bash
|
||||
-v /secure/config/empty-ca-dir:/home/agent/data/config/empty-ca-dir:ro \
|
||||
-e SSL_CERT_DIR=/home/agent/data/config/empty-ca-dir
|
||||
```
|
||||
|
||||
Do not use `SSL_CERT_DIR=`. Go treats an empty value as unset and scans its
|
||||
default certificate directories.
|
||||
|
||||
Only use that mode when the Agent does not need public roots for other TLS
|
||||
connections.
|
||||
|
||||
## Validate the live endpoints
|
||||
|
||||
### Strict TLS and identity check
|
||||
|
||||
Use only the specified bundle, without OpenSSL's default CA locations:
|
||||
|
||||
```bash
|
||||
openssl s_client \
|
||||
-brief \
|
||||
-connect 10.0.30.11:9554 \
|
||||
-verify_ip 10.0.30.11 \
|
||||
-verify_return_error \
|
||||
-CAfile "$HOME/.step/camera/uug-camera-trust-bundle.pem" \
|
||||
-no-CApath \
|
||||
-no-CAstore \
|
||||
</dev/null
|
||||
```
|
||||
|
||||
Repeat with port `443`. Both must report `Verification: OK`.
|
||||
|
||||
Confirm that identity checking is active by repeating the command with a wrong
|
||||
address, such as `-verify_ip 10.0.30.12`. It must fail with an IP address
|
||||
mismatch.
|
||||
|
||||
### Confirm the live leaf is the generated leaf
|
||||
|
||||
```bash
|
||||
live_fingerprint=$(
|
||||
openssl s_client -connect 10.0.30.11:9554 -showcerts </dev/null 2>/dev/null |
|
||||
openssl x509 -noout -fingerprint -sha256 |
|
||||
cut -d= -f2
|
||||
)
|
||||
|
||||
local_fingerprint=$(
|
||||
openssl x509 \
|
||||
-in "$HOME/.step/camera/bosch-rtsps-chain.pem" \
|
||||
-noout -fingerprint -sha256 |
|
||||
cut -d= -f2
|
||||
)
|
||||
|
||||
test "$live_fingerprint" = "$local_fingerprint"
|
||||
```
|
||||
|
||||
### Validate the media path
|
||||
|
||||
A successful TLS handshake does not prove that RTSP authentication, DESCRIBE,
|
||||
SETUP, PLAY, and RTP delivery work. Start a fresh Agent with verified TLS and
|
||||
confirm that it connects without an x509 error and receives frames. During the
|
||||
verified setup described here, a gortsplib probe completed all RTSP operations
|
||||
and received an RTP packet over TCP.
|
||||
|
||||
## Why a tampered bundle may still connect
|
||||
|
||||
Editing PEM text is not always a useful negative TLS test.
|
||||
|
||||
### A certificate can still parse after a byte change
|
||||
|
||||
Base64 can remain syntactically valid when one character changes. OpenSSL may
|
||||
still list the certificate subject and issuer even though a signature is now
|
||||
invalid. Parsing and signature verification are different operations.
|
||||
|
||||
### Trust anchors are not validated through a parent
|
||||
|
||||
Every certificate loaded into Go's root pool is a trust anchor, including a
|
||||
non-self-signed intermediate CA. Verification can terminate at that certificate.
|
||||
|
||||
If tampering changes only the intermediate's signature from its parent root,
|
||||
but does not change its public key, that intermediate can still validate the
|
||||
camera leaf when it is trusted directly. Its now-invalid parent signature is
|
||||
not consulted at the trust boundary.
|
||||
|
||||
This is equivalent to OpenSSL's partial-chain behavior:
|
||||
|
||||
```bash
|
||||
openssl s_client \
|
||||
-connect 10.0.30.11:9554 \
|
||||
-verify_ip 10.0.30.11 \
|
||||
-verify_return_error \
|
||||
-partial_chain \
|
||||
-CAfile tampered-bundle.pem \
|
||||
-no-CApath \
|
||||
-no-CAstore \
|
||||
</dev/null
|
||||
```
|
||||
|
||||
### `SSL_CERT_FILE` does not isolate Go from CA directories
|
||||
|
||||
On Unix, Go uses `SSL_CERT_FILE` instead of its default aggregate CA file, but it
|
||||
still scans default certificate directories such as `/etc/ssl/certs`. Setting
|
||||
`SSL_CERT_FILE` alone therefore does not remove CA certificates installed with
|
||||
`update-ca-certificates`.
|
||||
|
||||
Blank values do not select empty trust sources. Both `SSL_CERT_FILE=` and
|
||||
`SSL_CERT_DIR=` are treated as unset, so Go falls back to its default aggregate
|
||||
CA file and certificate directories. To test with no trusted certificates on
|
||||
Linux, use a non-empty file path that contains no certificates and a non-empty
|
||||
directory path that contains no certificates:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/empty-ca-dir
|
||||
SSL_CERT_FILE=/dev/null \
|
||||
SSL_CERT_DIR=/tmp/empty-ca-dir \
|
||||
AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=false \
|
||||
GOWORK=off \
|
||||
go run -tags moq . -action run -port 8080
|
||||
```
|
||||
|
||||
That fresh process must fail with `x509: certificate signed by unknown
|
||||
authority`.
|
||||
|
||||
Use exactly one trust-distribution approach when possible:
|
||||
|
||||
1. Mount a private trust bundle and set `SSL_CERT_FILE`; or
|
||||
2. Install the CA certificates into the operating-system trust store.
|
||||
|
||||
Using both is valid, but makes isolation tests less obvious.
|
||||
|
||||
### Running processes can retain old roots
|
||||
|
||||
A long-running Go process may already have loaded and cached the trust pool.
|
||||
Always start a new process after changing trust configuration during a negative
|
||||
test.
|
||||
|
||||
## Perform a meaningful negative test
|
||||
|
||||
Do not corrupt only the root or intermediate signature. Instead, give a fresh
|
||||
Agent process a completely unrelated CA and hide the default CA directories.
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/empty-ca-dir
|
||||
|
||||
openssl req \
|
||||
-x509 -newkey rsa:2048 -nodes -days 1 \
|
||||
-subj '/CN=Unrelated Test Root' \
|
||||
-keyout /tmp/unrelated-test-root.key \
|
||||
-out /tmp/unrelated-test-root.crt
|
||||
|
||||
SSL_CERT_FILE=/tmp/unrelated-test-root.crt \
|
||||
SSL_CERT_DIR=/tmp/empty-ca-dir \
|
||||
AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=false \
|
||||
GOWORK=off \
|
||||
go run -tags moq . -action run -port 8080
|
||||
```
|
||||
|
||||
The connection must fail with an unknown-authority or chain-building error.
|
||||
Delete the temporary test key and certificate afterward.
|
||||
|
||||
To test bundle integrity rather than client distrust, validate the intermediate
|
||||
against the root explicitly:
|
||||
|
||||
```bash
|
||||
openssl verify \
|
||||
-CAfile "$HOME/.step/certs/root_ca.crt" \
|
||||
-no-CApath \
|
||||
-no-CAstore \
|
||||
"$HOME/.step/certs/intermediate_ca.crt"
|
||||
```
|
||||
|
||||
Store and compare approved SHA-256 fingerprints when detecting unauthorized
|
||||
certificate-file changes is a requirement.
|
||||
|
||||
Restore an accidentally edited bundle from the protected CA certificates, then
|
||||
restart the Agent:
|
||||
|
||||
```bash
|
||||
step certificate bundle -f \
|
||||
"$HOME/.step/certs/intermediate_ca.crt" \
|
||||
"$HOME/.step/certs/root_ca.crt" \
|
||||
"$HOME/.step/camera/uug-camera-trust-bundle.pem"
|
||||
|
||||
openssl verify \
|
||||
-CAfile "$HOME/.step/certs/root_ca.crt" \
|
||||
-no-CApath \
|
||||
-no-CAstore \
|
||||
"$HOME/.step/certs/intermediate_ca.crt"
|
||||
```
|
||||
|
||||
## Optional system trust installation
|
||||
|
||||
On Debian, install both public CA certificates when every process in the system
|
||||
should trust this camera PKI:
|
||||
|
||||
```bash
|
||||
sudo install -m 0644 \
|
||||
"$HOME/.step/certs/root_ca.crt" \
|
||||
/usr/local/share/ca-certificates/uug-camera-ca.crt
|
||||
|
||||
sudo install -m 0644 \
|
||||
"$HOME/.step/certs/intermediate_ca.crt" \
|
||||
/usr/local/share/ca-certificates/uug-camera-intermediate-ca.crt
|
||||
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
This creates links below `/etc/ssl/certs`. Remove those files and rerun
|
||||
`update-ca-certificates` before attempting an isolated trust-bundle test.
|
||||
|
||||
## Renewal and recovery
|
||||
|
||||
- Renew before the leaf or intermediate expires.
|
||||
- Generate a new camera CSR if the firmware cannot renew the existing key.
|
||||
- Sign the new CSR with all required SANs.
|
||||
- Upload and validate the new certificate before deleting the old one.
|
||||
- Preserve an alternate administrative access path while changing HTTPS usage.
|
||||
- Back up the CA certificates, encrypted CA keys, and password separately.
|
||||
- If the CA private keys are lost, create a new CA and redistribute its trust
|
||||
before replacing camera certificates.
|
||||
|
||||
## Production checklist
|
||||
|
||||
- [ ] The Agent URL uses `rtsps://`, not `rtsp://` or `srtsp://`.
|
||||
- [ ] RTSPS is enabled on the camera and the configured port is reachable.
|
||||
- [ ] The certificate SAN exactly matches the Agent URL host.
|
||||
- [ ] The leaf public key matches the camera-generated CSR.
|
||||
- [ ] The certificate has `serverAuth` extended key usage.
|
||||
- [ ] The certificate expires before its issuer.
|
||||
- [ ] HTTPS is assigned to the private-CA certificate.
|
||||
- [ ] CBS client remains assigned to the Bosch device certificate.
|
||||
- [ ] The Agent has the intermediate and root CA certificates it needs.
|
||||
- [ ] `AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=false`.
|
||||
- [ ] The Agent was restarted after trust changes.
|
||||
- [ ] A strict TLS check reports `Verification: OK`.
|
||||
- [ ] A real Agent connection receives RTP packets.
|
||||
- [ ] CA private keys and passwords are backed up outside the devcontainer.
|
||||
20
README.md
20
README.md
@@ -189,6 +189,21 @@ Next to attaching the configuration file, it is also possible to override the co
|
||||
-e AGENT_CAPTURE_CONTINUOUS=true \
|
||||
-d --restart=always kerberos/agent:latest
|
||||
|
||||
### Secure camera streams (RTSPS)
|
||||
|
||||
The Agent accepts `rtsps://` camera URLs. Do not use `srtsp://`; RTSPS is RTSP over TLS. For a Bosch FLEXIDOME micro 3100i, enable **Secure RTSP** under **Network > Network Services** and use port `9554`:
|
||||
|
||||
```bash
|
||||
AGENT_CAPTURE_IPCAMERA_RTSP='rtsps://username:password@camera.example:9554/?inst=1'
|
||||
AGENT_CAPTURE_IPCAMERA_SUB_RTSP='rtsps://username:password@camera.example:9554/?inst=2'
|
||||
```
|
||||
|
||||
Certificate verification is enabled by default. The URL hostname or IP address must match the camera certificate SAN. On this Bosch firmware, RTSPS presents the certificate assigned to **HTTPS**; there is no separate SRTSP certificate usage. Leave **CBS client** assigned to the Bosch device certificate.
|
||||
|
||||
For a private CA, mount a PEM trust bundle containing every CA certificate needed to build the camera certificate chain and set `SSL_CERT_FILE` to that file. This Bosch firmware presents only its leaf certificate, so include both the issuing intermediate and root certificates in the bundle. As a temporary fallback, `AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE=true` disables certificate verification for camera streams only.
|
||||
|
||||
See [RTSPS and TLS certificates](README-RTSPS-TLS.md) for the complete Bosch UI, private-CA, deployment, validation, and troubleshooting procedure.
|
||||
|
||||
| Name | Description | Default Value |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `LOG_LEVEL` | Level for logging, could be "info", "warning", "debug", "error" or "fatal". | "info" |
|
||||
@@ -208,8 +223,9 @@ Next to attaching the configuration file, it is also possible to override the co
|
||||
| `AGENT_TIME` | Enable the timetable for Kerberos Agent | "false" |
|
||||
| `AGENT_TIMETABLE` | A (weekly) time table to specify when to make recordings "start1,end1,start2,end2;start1.. | "" |
|
||||
| `AGENT_REGION_POLYGON` | A single polygon set for motion detection: "x1,y1;x2,y2;x3,y3;... | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP or RTSPS endpoint for the target camera. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | RTSP or RTSPS sub-stream endpoint used for livestreaming (WebRTC). | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE` | Disable RTSPS camera certificate verification; use only when a trusted CA cannot be installed. | "false" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_BASE_WIDTH` | Force a specific width resolution for live view processing. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_BASE_HEIGHT` | Force a specific height resolution for live view processing. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" |
|
||||
|
||||
@@ -8,9 +8,11 @@ import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -38,6 +40,16 @@ import (
|
||||
|
||||
var tracer = otel.Tracer("github.com/kerberos-io/agent/machinery/src/capture")
|
||||
|
||||
const rtspsInsecureEnv = "AGENT_CAPTURE_IPCAMERA_RTSPS_INSECURE"
|
||||
|
||||
func rtspsTLSConfig() *tls.Config {
|
||||
if os.Getenv(rtspsInsecureEnv) != "true" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &tls.Config{InsecureSkipVerify: true} // #nosec G402 -- explicit opt-in for cameras with self-signed certificates
|
||||
}
|
||||
|
||||
// Implements the RTSPClient interface.
|
||||
type Golibrtsp struct {
|
||||
RTSPClient
|
||||
@@ -322,6 +334,7 @@ func (g *Golibrtsp) Connect(ctx context.Context, ctxOtel context.Context) (err e
|
||||
g.Client = gortsplib.Client{
|
||||
RequestBackChannels: false,
|
||||
Protocol: &protocol,
|
||||
TLSConfig: rtspsTLSConfig(),
|
||||
// Route gortsplib's packet-loss / decode-error reporting through our
|
||||
// structured logger with stream context (replaces its plain stdout
|
||||
// logging). These hooks are what let us tell whether the camera is
|
||||
@@ -599,6 +612,7 @@ func (g *Golibrtsp) ConnectBackChannel(ctx context.Context, ctxRunAgent context.
|
||||
g.Client = gortsplib.Client{
|
||||
RequestBackChannels: true,
|
||||
Protocol: &protocol,
|
||||
TLSConfig: rtspsTLSConfig(),
|
||||
}
|
||||
// parse URL
|
||||
u, err := base.ParseURL(g.Url)
|
||||
|
||||
22
machinery/src/capture/gortsplib_test.go
Normal file
22
machinery/src/capture/gortsplib_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package capture
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRTSPSTLSConfig(t *testing.T) {
|
||||
t.Run("verifies certificates by default", func(t *testing.T) {
|
||||
t.Setenv(rtspsInsecureEnv, "")
|
||||
|
||||
if got := rtspsTLSConfig(); got != nil {
|
||||
t.Fatalf("rtspsTLSConfig() = %#v, want nil", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("allows explicit insecure mode", func(t *testing.T) {
|
||||
t.Setenv(rtspsInsecureEnv, "true")
|
||||
|
||||
got := rtspsTLSConfig()
|
||||
if got == nil || !got.InsecureSkipVerify {
|
||||
t.Fatalf("rtspsTLSConfig() = %#v, want InsecureSkipVerify enabled", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user