fallback to init certs when provisioned certs don't exist
ac-client — USP Agent for OpenWrt Access Points
ac-client is a Rust daemon implementing the TR-369 / USP 1.3 Agent (User Services Platform, Broadband Forum) for OpenWrt-based access-point devices managed by an OptimACS controller (ac-server).
Getting Started with OptimACS
ac-client reports to an OptimACS controller. This section walks through creating an account and configuring your first tenant so devices can connect.
1. Create an account
Navigate to acs.optimcloud.com and click Create account on the sign-in page, or go directly to /signup.
Fill in the four sections of the signup form:
| Section | What to fill in |
|---|---|
| Your Account | First name, last name, work email, and a password (minimum 8 characters) |
| Your Organization | Company name, phone (optional), and country |
| Choose a Plan | Select the plan that fits your AP fleet size — you can upgrade at any time |
| Payment Method | Card details are collected via the Airwallex-hosted form. Card numbers are never stored on OptimACS servers — only a tokenized reference is saved in an isolated, encrypted PII database |
Click Create account. Your tenant is provisioned immediately and you are signed in automatically.
2. Manage your account
Access account settings at any time via the user menu → My Account in the top-right corner of the console.
Profile
Update your name, email address, and phone number.
Organization & Billing Address
Set your company name and billing address. This information appears on all invoices.
Security
Change your password using the live strength meter. Two-factor authentication via authenticator app is coming soon.
Danger Zone
Permanently delete your account and all associated data, or export a GDPR-compliant JSON copy of your personal data.
Deleting your account removes all access points, users, PII records, and billing data permanently. You must type
DELETEto confirm. This action cannot be undone.
3. Point ac-client at your tenant
Once your account is created, set server_host and server_cn in /etc/apclient/ac_client.conf on each device:
server_host = acs.optimcloud.com
server_cn = acs.optimcloud.com
The controller URL is the same for all tenants — authentication is handled via the device's provisioned client certificate issued by your tenant's step-ca instance.
Why ac-client? Why OptimACS?
The problem with managing fleets of access points
Running dozens — or thousands — of Wi-Fi access points across sites, campuses, or multi-tenant deployments is hard. Traditional approaches require vendor-specific NMS software, custom SSH scripting, or fragile SNMP polling. Devices drift from their intended configuration, firmware updates are manual and error-prone, and there is no standard way to query live device state from a remote controller.
TR-369 / USP: the open standard
The Broadband Forum's TR-369 User Services Platform (USP) defines a vendor-neutral, standards-based protocol for device management. A Controller (ac-server) pushes configuration, queries state, and triggers operations over a reliable, authenticated channel. An Agent (ac-client) on each device implements a structured TR-181 data model — a machine-readable, hierarchical representation of every configurable parameter on the device. Any controller that speaks USP can manage any agent that speaks USP, regardless of vendor.
Why Rust? Why open source?
- Memory safety: no buffer overflows, no use-after-free, no null-pointer crashes. The daemon runs on constrained MIPS/ARM hardware with limited RAM and no fault recovery — correct code matters.
- Minimal binary: the release build strips to a small self-contained binary with no runtime dependencies beyond musl libc. No Python, no JRE, no heavyweight runtime on the AP.
- Post-quantum TLS: ac-client uses
rustls-post-quantumto negotiate X25519 + ML-KEM-768 hybrid key exchange — a NIST PQC standard — on every connection. Device deployments live for years; their communications should be safe against harvest-now/decrypt-later attacks. - Standards compliance: the implementation is audited against the TR-369 v1.3 conformance requirements — Boot! event parameters, Record routing, WebSocket subprotocol enforcement, version negotiation, and error codes.
- Open source: the full protocol stack, data model, and OpenWrt packaging are available for inspection, extension, and contribution. No binary blobs, no vendor lock-in.
What OptimACS gives you out of the box
| Capability | Detail |
|---|---|
| Zero-touch provisioning | APs boot with a shared init cert; controller issues a unique per-device mTLS cert on approval |
| Live configuration push | SET any TR-181 parameter from the UI; agent applies via UCI and responds with SET_RESP |
| Firmware management | Upload firmware to the server; push via OPERATE Device.X_OptimACS_Firmware.Download() → sysupgrade |
| Real-time telemetry | ValueChange Notify every N seconds: uptime, load, free memory, GPS coords, wireless status |
| Camera management | Axis IP-camera discovery (ARP scan + CGI), periodic JPEG capture, image upload to server |
| Multi-tenant RBAC | Isolate fleets per tenant; role hierarchy from stats_viewer to super_admin |
| Post-quantum PKI | Smallstep step-ca issues all certs; CA private key never touches the controller |
Architecture
ac-client runs on each OpenWrt AP as a USP Agent. On first boot it connects using a shared bootstrap certificate, sends a Boot! Notify, and waits for the controller to issue it a unique per-device mTLS certificate. Thereafter it runs a continuous loop: handling incoming GET/SET/OPERATE messages, sending periodic ValueChange telemetry, and responding to firmware-upgrade and camera-capture operations.
ac-server is the Rust USP Controller. It listens on :3491 for incoming WebSocket connections and subscribes to EMQX for MQTT connections. It dispatches USP messages to the TR-181 data model, manages the device database, and delegates all X.509 certificate signing to step-ca via the JWK provisioner REST API.
step-ca (Smallstep) is the PKI. It issues the server TLS cert, per-device client certs, and the init bootstrap cert. The CA private key never leaves the step-ca container — ac-server holds only an EC P-256 JWK provisioner key to sign one-time tokens (OTTs) used to authenticate CSR signing requests.
optimacs-ui is the FastAPI + Jinja2 management console with Strawberry GraphQL. Real-time subscriptions update the dashboard, AP list, and USP event log automatically.
EMQX provides the MQTT 5 broker for the MQTT Message Transport Protocol (MTP). Agents and the controller exchange USP Records via MQTT topics:
usp/v1/{agent_endpoint_id} ← agent subscribes (receives Controller messages)
usp/v1/{controller_endpoint_id} ← controller subscribes (receives Agent messages)
System Components
| Component | Role | Port(s) |
|---|---|---|
| ac-server | USP Controller, provisioning, TR-181 dispatch | 3491 (WSS) |
| step-ca | PKI / Certificate Authority | 9000 (HTTPS) |
| optimacs-ui | Management web console (FastAPI + GraphQL) | 8080 |
| EMQX | MQTT broker (USP MQTT MTP) | 1883, 8883, 8083, 8084, 18083 |
| MariaDB / MySQL | Device and configuration database | 3306 |
| Redis | Config-proto cache, rate-limit store (optional) | 6379 |
| ac-client | USP Agent on each OpenWrt AP | (outbound only) |
TR-369 / USP Protocol
Conformance: ac-client implements TR-369 USP 1.3 (Broadband Forum, November 2023). The implementation passes all mandatory requirements for the WebSocket and MQTT MTPs.
Wire Format
USP uses Protocol Buffers (proto3). Two proto files are vendored in proto/:
| File | Purpose |
|---|---|
proto/usp-record.proto |
USP Record envelope — version, to_id/from_id endpoint IDs, MTP connect records |
proto/usp-msg.proto |
USP Message body — GET/SET/OPERATE/NOTIFY and their responses |
Message Types
| Message | Direction | Purpose |
|---|---|---|
GET |
Controller → Agent | Read TR-181 parameter values (respects max_depth) |
GET_RESP |
Agent → Controller | Parameter values |
SET |
Controller → Agent | Write TR-181 parameter values |
SET_RESP |
Agent → Controller | Acknowledgement with populated updated_obj_results |
OPERATE |
Controller → Agent | Execute a command |
OPERATE_RESP |
Agent → Controller | Command output args |
NOTIFY (Boot!) |
Agent → Controller | Device boot event; obj_path="Device.", includes Cause + FirmwareUpdated |
NOTIFY (ValueChange) |
Agent → Controller | Periodic telemetry (UpTime, LoadAvg, GPS, etc.) |
NOTIFY_RESP |
Controller → Agent | Acknowledge notify |
GET_SUPPORTED_PROTO |
Agent → Controller | Negotiate USP version; result stored and applied to Records |
Error 7004 |
Agent → Controller | Returned for unsupported message types (NOT_SUPPORTED) |
TR-369 Conformance Notes
| Requirement | Implementation |
|---|---|
| §10.2.1 WebSocket subprotocol | Server enforces and echoes Sec-WebSocket-Protocol: v1.usp; client verifies echo |
| §5.1 Record routing | Records with to_id ≠ own endpoint ID are logged and discarded |
| §6.2.1 Version negotiation | GetSupportedProtoResp version stored and used in subsequent Records |
| §9.3.6 Boot! event | obj_path="Device.", required Cause and FirmwareUpdated params included |
| §6.2.4 SET_RESP | updated_obj_results populated with one entry per updated object path |
| §6.1.2 GET max_depth | max_depth extracted and applied to DM path depth filtering |
| §6.4 Error codes | Error 7004 (NOT_SUPPORTED) returned for known-unsupported message types |
Provisioning Flow
Agent (new device) Controller (ac-server)
│ │
│── WebSocketConnectRecord ─────────────▶│
│── Notify { Boot!, DeviceInfo.* } ─────▶│ → new_systems table
│ │ (admin approves in UI)
│◀─ OPERATE IssueCert() ────────────────│
│── OPERATE_RESP { csr: "..." } ─────────▶│ → sign cert via step-ca
│◀─ SET Security.{CaCert,Cert,Key} ─────│
│ apply::save_certs() │
│── [reconnect with device cert] ────────▶│ → provisioned
│ │
│── Notify { ValueChange, UpTime=... } ──▶│ periodic telemetry
TR-181 Data Model
The TR-181 Device:2 subset exposed by ac-client:
| TR-181 Path | RW | Source |
|---|---|---|
Device.DeviceInfo.HostName |
RW | UCI / hostname |
Device.DeviceInfo.SoftwareVersion |
RO | /etc/openwrt_release |
Device.DeviceInfo.HardwareVersion |
RO | arch string |
Device.DeviceInfo.SerialNumber |
RO | MAC address |
Device.DeviceInfo.UpTime |
RO | /proc/uptime |
Device.DeviceInfo.X_OptimACS_LoadAvg |
RO | /proc/loadavg |
Device.DeviceInfo.X_OptimACS_FreeMem |
RO | /proc/meminfo |
Device.DeviceInfo.X_OptimACS_Latitude |
RO | GNSS reader |
Device.DeviceInfo.X_OptimACS_Longitude |
RO | GNSS reader |
Device.WiFi.Radio.{i}.Channel |
RW | UCI wireless |
Device.WiFi.Radio.{i}.Enable |
RW | UCI wireless |
Device.WiFi.SSID.{i}.SSID |
RW | UCI wireless |
Device.WiFi.AccessPoint.{i}.Security.KeyPassphrase |
RW | UCI wireless |
Device.WiFi.AccessPoint.{i}.Security.ModeEnabled |
RW | UCI wireless |
Device.IP.Interface.{i}.IPv4Address.{i}.IPAddress |
RW | UCI network |
Device.IP.Interface.{i}.IPv4Address.{i}.SubnetMask |
RW | UCI network |
Device.IP.Interface.{i}.IPv4Address.{i}.AddressingType |
RW | UCI network |
Device.DHCPv4.Server.Pool.{i}.StaticAddress.{i}.* |
RW | UCI dhcp |
Device.Hosts.Host.{i}.* |
RW | UCI hosts |
Device.X_OptimACS_Camera.{i}.* |
RO | Axis CGI discovery |
Device.X_OptimACS_Camera.{i}.Capture() |
OP | JPEG capture + upload |
Device.X_OptimACS_Firmware.AvailableVersion |
RO | server firmware table |
Device.X_OptimACS_Firmware.Download() |
OP | sysupgrade |
Device.X_OptimACS_Security.IssueCert() |
OP | PKI cert issuance |
Security Architecture
Transport Security
- TLS 1.3 with mutual authentication on all connections to ac-server
- Post-quantum hybrid key exchange: X25519 + ML-KEM-768 (NIST FIPS 203, ML-KEM). Deployed in every ac-client binary — device traffic is safe against harvest-now/decrypt-later attacks
- Mutual TLS: both client and server present X.509 certificates; the server rejects any connection without a valid client certificate signed by the trusted CA
- No hostname verification on client cert: ac-client uses a custom
AcpServerVerifierthat validates the full certificate chain but matches the server by CA trust rather than CN — consistent with how OpenSSLSSL_VERIFY_PEERworked in the original C client
Certificate Lifecycle
Bootstrap (every new device):
ac-client ships with a shared init certificate (00:00:00:00:00:00)
This cert allows it to connect and register — but nothing else.
Provisioning (one-time, admin-triggered):
1. AP connects → sends Boot! Notify with DeviceInfo parameters
2. Appears in controller's New Systems queue
3. Admin approves in the OptimACS UI
4. Controller sends OPERATE Device.X_OptimACS_Security.IssueCert()
5. Agent generates an RSA key pair + CSR; returns CSR in OPERATE_RESP
6. Controller signs a JWT one-time token (OTT) with its EC P-256 JWK provisioner key
7. Controller forwards CSR + OTT to step-ca (POST /1.0/sign)
step-ca verifies OTT, issues a unique per-device certificate
8. Controller sends SET {CaCert, Cert, Key} to the agent
9. Agent writes certs to /etc/apclient/certs/ and reconnects
Post-provisioning:
Every connection uses the device's unique mTLS cert.
The init cert is no longer accepted for this device's endpoint ID.
Revocation:
Removing a device from the UI prevents future connections.
The cert is not added to a CRL — access is controlled at the
application layer by endpoint ID lookup in the database.
Why step-ca?
The CA private key never touches ac-server. ac-server holds only the EC P-256 JWK provisioner key — a narrow credential that can only sign one-time tokens used to authenticate CSR requests. This means:
- A compromised ac-server cannot forge device certificates
- The CA can be rotated independently of the controller
- step-ca's audit log provides a full record of every certificate issued
- In Kubernetes deployments, the step-ca pod can be isolated in its own namespace with network policies that allow only ac-server to reach the signing API
Security Posture Summary
| Property | Value |
|---|---|
| TLS version | 1.3 (minimum enforced by rustls) |
| Key exchange | X25519 + ML-KEM-768 (post-quantum hybrid) |
| Client authentication | Mutual TLS — X.509 cert signed by step-ca root CA |
| CA key isolation | step-ca holds root key; ac-server holds only JWK provisioner key |
| Certificate issuance | OTT-authenticated CSR signing via step-ca REST API |
| Binary memory safety | Rust — no buffer overflows, no use-after-free |
| Credential storage | Certs written to /etc/apclient/certs/ (mode 0600) |
Features
- TR-369 / USP 1.3 conformant Agent (Boot! Notify, GET, SET, OPERATE)
- WebSocket MTP and MQTT MTP — configurable, or both simultaneously
- Mutual TLS with post-quantum hybrid key exchange (X25519 + ML-KEM-768) via
rustls-post-quantum - UCI-backed TR-181 data model — Device.DeviceInfo, Device.WiFi, Device.IP, Device.Hosts, Device.DHCPv4
- Vendor extensions:
Device.X_OptimACS_Camera.*,Device.X_OptimACS_Firmware.*,Device.X_OptimACS_Security.* - Two-phase provisioning: bootstrap cert → controller-issued mTLS cert lifecycle
- Firmware upgrade via sysupgrade
- Axis IP-camera discovery (ARP scan + CGI API) and JPEG upload
- GNSS telemetry (NMEA serial reader)
- ValueChange periodic telemetry (uptime, load, GPS, wireless, modem)
- OpenWrt package feed entry (
package/ac-client/) for cross-compilation viarust-package.mk
Repository Layout
ac-client/
├── src/
│ ├── main.rs — tokio runtime, load config, spawn agent
│ ├── config.rs — parse ac_client.conf + MtpType enum
│ ├── apply.rs — apply_config(), save_certs(), apply_firmware()
│ ├── cam.rs — Axis camera discovery + JPEG capture
│ ├── gnss.rs — GNSS position reader (NMEA serial)
│ ├── tls.rs — mutual TLS client connector
│ ├── util.rs — read_uptime(), read_fw_version(), MAC detection, etc.
│ └── usp/
│ ├── mod.rs — UspError, proto includes
│ ├── agent.rs — main USP agent loop
│ ├── record.rs — encode/decode USP Records
│ ├── message.rs — builder helpers (Boot!, ValueChange, etc.)
│ ├── endpoint.rs — EndpointId from MAC
│ ├── session.rs — sequence_id counter
│ ├── dm/ — TR-181 data model (UCI-backed)
│ │ ├── mod.rs — DmCtx, get_params(), set_params(), operate()
│ │ ├── device_info.rs — Device.DeviceInfo.*
│ │ ├── wifi.rs — Device.WiFi.* via UCI
│ │ ├── ip.rs — Device.IP.Interface.*
│ │ ├── dhcp.rs — Device.DHCPv4.*
│ │ ├── hosts.rs — Device.Hosts.Host.*
│ │ ├── cameras.rs — Device.X_OptimACS_Camera.*
│ │ ├── firmware.rs — Device.X_OptimACS_Firmware.*
│ │ └── security.rs — Device.X_OptimACS_Security.*
│ └── mtp/
│ ├── websocket.rs — WSS client with reconnect loop
│ └── mqtt.rs — rumqttc MQTT client
├── proto/ — vendored Protocol Buffer schemas
│ ├── acp.proto — OptimACS control protocol
│ ├── usp-record.proto — TR-369 USP Record wire format
│ └── usp-msg.proto — TR-369 USP Message types
├── build.rs — prost-build codegen for proto files
├── Cargo.toml
├── Cargo.lock
└── package/
└── ac-client/ — OpenWrt package feed entry
├── Makefile — OpenWrt package definition (rust-package.mk)
└── files/
├── ac-client.init — procd init script
└── ac_client.conf — default configuration
Building
Native (host) build
Requirements: Rust stable ≥ 1.75, cmake, clang
cargo build --release
cargo test
Output: target/release/ac-client
Cross-compile for OpenWrt
Use the OpenWrt buildroot with the package/ac-client/ feed entry (see OpenWrt Package below).
OpenWrt Package
The package/ac-client/ directory is an OpenWrt package feed entry that cross-compiles ac-client for any OpenWrt target architecture (MIPS, ARM, AArch64, x86_64).
Requirements
- OpenWrt 22.03 or later (musl 1.2 + kernel headers)
- Rust host toolchain from
packages/lang/rustin the packages feed cmakeon the build host — pulled in automatically viaHOST_BUILD_DEPENDS
Add to your buildroot
# 1. Register the feed in feeds.conf
echo "src-git-full ac-client git@github.com:optim-enterprises-bv/ac-client.git" >> feeds.conf
# 2. Update and install the feed
./scripts/feeds update ac-client
./scripts/feeds install ac-client
# 3. Select the package
make menuconfig
# Network → Management → ac-client [*]
# 4. Build
make package/ac-client/compile V=s
Note: Use
src-git-full(notsrc-git) so the full repo history is cloned — required for subdir-based builds.
Installed files
| Path | Description |
|---|---|
/usr/sbin/ac-client |
Daemon binary |
/etc/apclient/ac_client.conf |
Default configuration (preserved across upgrades) |
/etc/init.d/ac-client |
procd init script (respawning, logs to syslog) |
/etc/apclient/init/ |
Directory for the bootstrap certificate |
/etc/apclient/certs/ |
Directory for the provisioned client certificate |
Certificate Deployment
Default bootstrap certificates (out-of-box)
The OpenWrt package ships with a set of default bootstrap certificates in package/ac-client/files/init/. These are installed to /etc/apclient/init/ automatically during opkg install, allowing ac-client to start and attempt provisioning immediately after flashing — no manual cert deployment required for initial bring-up.
| File | CN | Purpose |
|---|---|---|
ca.crt |
OptimACS Default Bootstrap CA |
Verifies the server certificate chain |
client.crt |
00:00:00:00:00:00 |
Default init identity presented during INIT handshake |
client.key |
— | Private key for client.crt |
Security notice: These certificates are public — the key material is included in the open-source repository and must be considered known to any third party. They are suitable for development, lab bring-up, and initial provisioning only. Replace them with certificates from your own step-ca before connecting devices to a production controller.
Production certificate deployment
For production, overwrite the default files with certificates issued by your OptimACS server's step-ca instance:
# Copy from the server's peer directory for the default init CN
scp <server>:/var/ac-server/peers/00:00:00:00:00:00/client.crt \
root@<ap>:/etc/apclient/init/client.crt
scp <server>:/var/ac-server/peers/00:00:00:00:00:00/client.key \
root@<ap>:/etc/apclient/init/client.key
scp <server>:/etc/optimacs/CA/rootCA.crt \
root@<ap>:/etc/apclient/init/ca.crt
/etc/init.d/ac-client enable
/etc/init.d/ac-client start
Because these three files are listed as
conffilesin the package,opkg upgradewill never overwrite operator-deployed certificates.
Configuration
ac-client reads /etc/apclient/ac_client.conf (key = value format, # comments).
/etc/init.d/ac-client restart # after config changes
TLS / Certificates
| Key | Default | Description |
|---|---|---|
init_cert |
/etc/apclient/init/client.crt |
Bootstrap certificate (pre-provisioning) |
init_key |
/etc/apclient/init/client.key |
Bootstrap private key |
ca_file |
/etc/apclient/init/ca.crt |
CA certificate for server verification |
cert_file |
/etc/apclient/certs/client.crt |
Provisioned client certificate |
key_file |
/etc/apclient/certs/client.key |
Provisioned client private key |
cert_dir |
/etc/apclient/certs |
Directory where provisioned certs are saved |
Connection
| Key | Default | Description |
|---|---|---|
server_host |
acs.optimcloud.com |
ac-server hostname or IP |
server_cn |
acs.optimcloud.com |
Expected CN in the server TLS certificate (SNI) |
mtp |
websocket |
MTP selection: websocket | mqtt | both |
ws_url |
wss://acs.optimcloud.com:3491/usp |
WebSocket MTP URL |
mqtt_url |
mqtt://acs.optimcloud.com:1883 |
MQTT broker URL |
mqtt_client_id |
(auto) | MQTT client identifier |
Device Identity
| Key | Default | Description |
|---|---|---|
mac_addr |
(auto) | MAC address — auto-detected from br-lan/eth0/wlan0 |
usp_endpoint_id |
(auto) | USP Endpoint ID — auto-generated as oui:{oui}:{mac} |
controller_id |
oui:00005A:OptimACS-Controller-1 |
Controller endpoint ID |
Telemetry
| Key | Default | Description |
|---|---|---|
status_interval |
300 |
Seconds between ValueChange Notify messages |
gnss_dev |
(disabled) | Serial device for NMEA GPS (e.g. /dev/ttyUSB0) |
gnss_baud |
9600 |
GNSS baud rate |
Storage Paths
| Key | Default | Description |
|---|---|---|
fw_dir |
/tmp/apclient/firmware |
Scratch directory for downloaded firmware |
img_dir |
/var/apclient/images |
Directory for saved camera snapshots |
pid_file |
/var/run/apclient.pid |
PID file path |
Process Behaviour
| Key | Default | Description |
|---|---|---|
daemonize |
false |
Background daemon mode (leave false under procd) |
log_syslog |
true |
Log to syslog (true) or stderr (false) |
Protocol Details
Device Lifecycle
Phase 1 — Provisioning
Connect with init cert → Boot! Notify → controller approves → receive CERT
→ save to cert_dir → reconnect with provisioned cert
Phase 2 — Operation
Boot! Notify → GET/SET/OPERATE dispatch loop
→ ValueChange Notify every status_interval seconds
→ Firmware upgrade via OPERATE Device.X_OptimACS_Firmware.Download()
→ Camera cycle every cam_interval seconds
USP Endpoint ID
Auto-generated from the device MAC address:
oui:{vendor-oui}:{mac-address-without-colons}
# e.g. oui:0060B3:aabbccddeeff
License
Copyright (c) 2026 Optim Enterprises BV. Released under the BSD 3-Clause License.






