Security
This page is the operator's guide to securing the link between the RIC and its xApps: what the transport protects, how to turn mutual TLS on and off, how to issue and rotate certificates, and how to read the failures when something is wrong.
What is protected, and what is not
The RIC has two independent secured surfaces, each with its own certificate authority. Keep them separate — a certificate from one is not valid on the other, by design.
| Surface | Port | Faces | Protected by |
|---|---|---|---|
| xApp IPC | 36422 | xApps (this page) | mutual TLS over tls://, RIC CA |
| A1-P policy API | 8443 | SMO / Non-RT RIC | mutual TLS + optional OAuth2, A1 CA |
| E2AP | 36421/sctp | E2 nodes (gNB) | not encrypted — see below |
/metrics |
9095 | Prometheus | not authenticated — bind it privately |
Enabling mTLS on the xApp IPC gives you three things:
- Confidentiality and integrity of everything between the RIC and its xApps — subscriptions, indications, control, and the measurement data they carry.
- Authentication of both ends. The RIC will not accept a client that cannot present a certificate issued by its CA, and the xApp will not talk to a RIC whose certificate does not chain to that same CA and match the address it dialled.
- Identity binding. A certificate authenticates exactly one xApp: the
certificate's Common Name must equal the xApp's
deployment_name, so a certificate issued for one xApp cannot be used to register as another.
It deliberately does not give you:
- Capability authorization. Every authenticated xApp may use every RAN function and every operation. mTLS answers "who are you", not "what may you do".
- Workload attestation. The certificate proves possession of a key file, not that a particular container image is running.
- Automatic certificate rotation. See Certificate lifetime — this needs a calendar entry today.
- E2AP protection. The southbound SCTP link to the gNB is not encrypted by the RIC; isolate it at the network layer.
mTLS is all-or-nothing per RIC
The RIC has one xApp IPC listener with one scheme. If it is on tls://,
every xApp must also be on tls:// — a plaintext xApp cannot connect
to a TLS listener, and vice versa. There is no mixed mode and no gradual
migration path, so plan the switch as one coordinated change.
Choosing a transport
transport.northbound.socket (RIC) and ric.endpoint (xApp) select the transport by
URI scheme. Both sides must agree.
| Scheme | Use when | Authentication |
|---|---|---|
unix:///run/airpuls/ric.sock |
xApps share the host with the RIC | filesystem permissions |
tcp://<ip>:36422 |
trusted network, lab, or development | none |
tls://<ip>:36422 |
anything crossing a host or an untrusted network | mutual TLS |
The shipped default is tcp://, so mTLS is off until you turn it on.
Prerequisites
Mutual TLS needs the GIO TLS backend in every image that participates —
glib-networking. Without it the code compiles and starts, but every TLS
operation fails at runtime with a clear error. It is included in the RIC
image, the xApp SDK base image, and the CI test image.
If you build your own images, rebuild them after upgrading to a release that
supports tls://, and rebuild the SDK base image before the xApp images
that derive from it.
Enabling mutual TLS
The four steps below are the whole procedure. Nothing in it requires rebuilding the RIC — the scheme is read from configuration at startup.
1. Issue the certificates
In the deployment repository (nrtric-ctrl), setup-ric-security.sh mints
the RIC trust domain into conf/ric-tls/. start.sh runs it automatically,
and it is safe to run by hand:
It produces, all with 825-day lifetimes:
| File | Used by | Purpose |
|---|---|---|
ca.crt / ca.key |
(the CA — ca.key is never mounted) |
signs everything below |
server.crt / server.key |
RIC | the listener's identity |
<xapp>.crt / <xapp>.key |
that xApp | one client identity per xApp |
The listener certificate carries SANs for localhost, airpuls-ric,
127.0.0.1, and 174.19.19.100. If xApps reach the RIC by any other
address, add it before the first run:
The script is incremental: it mints only what is missing, so re-running it never disturbs an existing CA or a working certificate.
The Common Name is a contract, not a label
Each client certificate's CN must equal that xApp's
xapp.deployment_name. The RIC compares the two and refuses any
connection where they differ. setup-ric-security.sh follows this
automatically; if you bring your own PKI, you must too.
2. Point the RIC at the material
In conf/nrtric.yml, switch the scheme and supply the paths:
transport:
northbound:
socket: "tls://0.0.0.0:36422"
cert_file: /etc/airpuls/tls/server.crt
key_file: /etc/airpuls/tls/server.key
client_ca_file: /etc/airpuls/tls/ca.crt
require_cn_match: true # default; see below
All three paths travel as a unit — a partial block fails at startup. The
paths are the container's view; the compose file mounts each file
read-only from conf/ric-tls/, and deliberately never mounts ca.key.
3. Point each xApp at its own certificate
In every xApp's config (conf/<xapp>.yml, mounted as xapp.yml):
ric:
endpoint: "tls://airpuls-ric:36422"
tls:
ca_file: /etc/airpuls/tls/ca.crt
cert_file: /etc/airpuls/tls/y1-termination.crt
key_file: /etc/airpuls/tls/y1-termination.key
xapp:
deployment_name: "y1-termination" # must equal the certificate CN
All three paths are required. ca_file has no default: the RIC's
certificate comes from a private CA that no public trust store carries, so
there is nothing sensible to fall back to.
Add ric.tls.server_name only when the xApp dials an address that is not in
the listener certificate's SAN list and you cannot re-issue the certificate.
4. Restart and verify
./start.sh # mints anything missing, then brings the stack up
docker logs airpuls-ric | grep -i tls
docker logs y1-termination | tail
A healthy listener logs its credentials once at startup and then one line per xApp as it connects:
[INFO] [TLS ] credentials loaded (cert=/etc/airpuls/tls/server.crt, ca=/etc/airpuls/tls/ca.crt, require_peer=yes)
[INFO] [xapp-ipc] xapp_ipc: listening on tls://0.0.0.0:36422 (n_shards=1, mode=single-thread)
[INFO] [xapp-ipc] xapp_ipc: TLS established (peer=CN=y1-termination)
That last line is the proof: the RIC saw a verified certificate and read its
identity from it. Confirm the xApp then registers as usual, and that
airpuls_xapps_registered on /metrics reaches the expected count.
Disabling mutual TLS
There is no build-time switch to undo. Disabling is the exact inverse of enabling: the endpoint scheme and the TLS material move together, always.
conf/nrtric.yml→transport.northbound.socket: "tcp://0.0.0.0:36422"and comment the northbound TLS keys.- Every xApp's
ric.endpoint→tcp://airpuls-ric:36422and comment theric.tlsblock. - Restart the RIC and every xApp together — remember the all-or-nothing rule.
The certificates in conf/ric-tls/ stay on disk; they cost nothing while
unused and mean re-enabling is a config change only.
Scheme and material are never changed independently
A scheme without material, or material without a scheme, is a half-finished change, not a configuration style. Each side says so:
northbound TLS material configured but transport.northbound.socket (…) is not tls:// — TLS material unused
ric.tls configured but ric.endpoint (…) is not tls:// — TLS material unused
Treat either line as an unfinished edit and complete it. The reverse
order — a tls:// scheme with no material — is fatal at startup rather
than a warning, because it would otherwise leave the RIC with no
listener at all. Deleting part of a tls block is likewise rejected:
the three file keys are present together or absent together.
Onboarding a new xApp
With mTLS on, a new xApp needs an identity before it can connect. Mint it
under the name the xApp calls itself — the certificate CN must equal its
xapp.deployment_name, which the RIC checks before registering it:
This mints kpm-monitor-xapp.crt / .key signed by the existing CA and
touches nothing else. Because the RIC trusts any certificate chaining to its
CA, the new certificate is accepted immediately — no RIC restart, and no
restart of the other xApps.
Then give the xApp its material, which differs by how it is deployed:
- Add read-only mounts for its own
.crt/.keyplusca.crtto its service (copy they1-terminationblock — mount only that xApp's own key, neverca.key). - Set
ric.endpointtotls://…and fill inric.tlsas above.
- Uncomment the
tls://endpoint and theric.tlsblock in the xApp'sxapp.yml— both ship commented out beside the plaintext form. -
Launch with
RIC_TLS_DIRpointing at the PKI directory:The launcher mounts the three files individually and runs the container as the invoking user so the
0600key is readable. It refuses to start, naming the missing file, if the certificate has not been minted yet.
Either way, xapp.deployment_name must match the certificate CN exactly.
Certificate lifetime and rotation
Everything minted in one run shares an 825-day lifetime (~27 months), so the CA, the listener certificate and every client certificate expire on roughly the same day. There is no automatic renewal, and the RIC and xApps read their certificates once at startup.
Put the expiry in your calendar
Nothing in the system warns you before certificates expire. When they do,
every xApp fails its handshake at once. Record the date from
openssl x509 -in conf/ric-tls/ca.crt -noout -enddate when you first run
the setup script.
Rotation is manual but does not require re-rolling everything:
| Goal | Procedure | Impact |
|---|---|---|
| Re-issue one xApp's certificate | delete conf/ric-tls/<xapp>.{crt,key}, re-run the script |
restart that xApp only |
| Change the listener's SAN or key | delete conf/ric-tls/server.{crt,key}, re-run with RIC_TLS_SAN=… |
restart the RIC only |
| Roll the CA | delete all of conf/ric-tls/, re-run the script |
restart the RIC and every xApp |
Only the last row is a fleet-wide event, because the CA is the trust anchor both sides verify against.
Key handling
- Private keys are created
0600inside a0700directory and are never world-readable, even if the script is interrupted. conf/ric-tls/is git-ignored. Never commit it.- Containers mount only the files they need: the RIC gets
server.crt,server.key,ca.crt; each xApp gets its own pair plusca.crt.ca.keyis mounted nowhere — a compromised container therefore cannot mint new identities. - The RIC and the xApps run as the invoking user rather than root, so the
0600mode is a real barrier. - Mounts use
create_host_path: false: if the material is missing, the container fails to start rather than silently receiving an empty root-owned directory. Run./start.sh(or the setup script) at least once beforedocker compose up.
Identity binding
require_cn_match (default true) makes the certificate CN the
authoritative identity. On every connection the RIC compares the CN of the
verified client certificate with the deployment_name in the xApp's Hello
and refuses a mismatch before the xApp is registered.
Turn it off (require_cn_match: false) only when using a foreign PKI
whose CNs cannot follow the convention. Doing so means any certificate your
CA has issued can register under any xApp name.
Troubleshooting
Each row is a message you will actually see in the logs.
| Message | Cause | Fix |
|---|---|---|
no GIO TLS backend (is glib-networking installed?) |
image lacks glib-networking |
rebuild the image (SDK base first for xApps) |
xapp_ipc: … requires TLS material (transport.northbound …) |
tls:// scheme with no/partial material |
supply all three paths |
transport.northbound.cert_file '…' is not readable |
wrong path, or the mount is missing | check the path and the compose mount |
transport.northbound: cert_file, key_file and client_ca_file must all be set together |
partial block | set all three |
transport.northbound.socket uses tls:// but the northbound TLS material is not fully configured |
scheme without material | add the block, or revert the scheme |
xApp IPC tls:// listener failed to start on … |
credentials unusable, or no TLS backend | read the preceding error line |
identity mismatch — certificate CN="a" but Hello claims deployment_name="b" |
certificate CN ≠ deployment_name |
re-issue the certificate, or fix the name |
TLS connection peer did not send a certificate |
xApp connected without a client certificate | fill in the xApp's ric.tls |
peer certificate rejected (errors=0x…, subject=…) |
certificate not signed by the configured CA (or expired) | issue from the right CA; check expiry |
ric_client_connect: tls:// endpoint requires ric.tls.ca_file, ric.tls.cert_file and ric.tls.key_file |
xApp config incomplete | supply all three |
ric_client_connect: ric.endpoint must use tcp://, unix:// or tls:// scheme |
typo in the scheme | correct the URI |
ric_client_connect: ric.tls.ca_file, ric.tls.cert_file and ric.tls.key_file must all be set together |
partial xApp block | set all three, or remove all three |
ric.tls configured but ric.endpoint (…) is not tls:// — TLS material unused |
half-finished enable or disable on the xApp side | move scheme and material together |
northbound TLS material configured but transport.northbound.socket (…) is not tls:// — TLS material unused |
listener counterpart of the row above | same |
TLS handshake with …:… failed: … |
peer mismatch, wrong CA, wrong SAN, or a plaintext RIC | check both ends use tls:// and share a CA |
| xApp connect hangs ~5 s then fails | RIC not answering TLS on that port | confirm the listener scheme and port |
bind source path does not exist at docker compose up |
conf/ric-tls/ missing |
run ./start.sh or the setup script once |
Two quick checks that resolve most cases:
# Does the client certificate chain to the CA the RIC trusts?
openssl verify -CAfile conf/ric-tls/ca.crt conf/ric-tls/y1-termination.crt
# Is the CN what the xApp calls itself?
openssl x509 -in conf/ric-tls/y1-termination.crt -noout -subject
Related
- Configuration — every RIC configuration key.
- Docker Deployment — images, ports, compose layout.
- SDK Configuration — the xApp side of
ric.tls.