# iSecOS Agent ↔ Control Center Protocol **Version:** 1.0 (MVP, lab) · **Spec:** `docs/control-center-spec.md` §8.3 · **Server implementation:** `os/control-center/src/api/routes/agent.py` · **Agent implementation:** `os/config-shared/config/hooks/live/066-cc-agent-shared.chroot` This document describes how the `isecos-agent` service on every iSecOS endpoint communicates with the iSecOS Control Center (CC). - **Base URL (lab):** `http://10.0.2.2:8000/api` (QEMU user-net → host) - **Base URL (production):** `https://isecos.isecurity.vn/api` (nginx gateway → CC `:8000`) - **Content type:** `application/json` on all requests with a body - **Client config:** `/etc/isecos/cc-agent.conf` (`CC_URL`, `ENROLL_KEY`) - **Persistent identity:** `/etc/isecos/cc-agent.id` (holds `enrollment_id`, mode `0600`) ## 1. Authentication All agent endpoints require a shared enroll key, sent as a Bearer token: ```http Authorization: Bearer ``` - The key is compared in **constant time** (`secrets.compare_digest`). - Lab default: `isecos-agent-lab-key` (set via `ISEC_AGENT_ENROLL_KEY` env on the CC). - Production: generate a random key (`openssl rand -hex 24`), distribute it to endpoints over a separate channel, and set it in the CC environment/compose file. - **Roadmap (Phase 3):** mTLS with per-enrollment client certificates issued by the CC CA (spec §6.1). The Bearer key is the MVP bridge. | Error | Meaning | |---|---| | `401 {"detail": "invalid enroll key"}` | Missing/incorrect `Authorization` header | ## 2. Lifecycle overview ``` Boot ──► enroll (once, stores enrollment_id) ──► every 5 min: checkin ──► policy pull ──► apply locally ``` The agent is a pure-bash oneshot (`curl` + coreutils) driven by systemd: `isecos-agent.service` (`Type=oneshot`) + `isecos-agent.timer` (`OnBootSec=2min`, `OnUnitActiveSec=5min`). --- ## 3. Enroll — `POST /api/agent/enroll` Called **once per machine** (first boot or after `/etc/isecos/cc-agent.id` is deleted). Returns a UUID `enrollment_id` that permanently identifies the endpoint. **Request** ```http POST /api/agent/enroll Authorization: Bearer isecos-agent-lab-key Content-Type: application/json { "hostname": "ws-hn-014", "os_version": "iSecOS 1.0-alpha6 (MotaX)", "kernel_version": "6.12.0-1-amd64", "mac_address": "52:54:00:ab:cd:ef" } ``` Only `hostname` is required; the other fields are optional. **Response `200`** ```json { "enrollment_id": "3f1c2b8e-9a44-4d17-a2f5-6b0c9e8d7a11", "server_time": "2026-11-06T08:15:30.123456+00:00" } ``` **Side effects on the server** - New row in `endpoints` (`status=online`, `last_seen=now`). - Row `event="enroll"` in `agent_events` (visible in portal → AuditLog). **Errors** | Status | Body | When | |---|---|---| | 401 | `{"detail": "invalid enroll key"}` | bad Bearer token | | 422 | FastAPI validation detail | missing `hostname` / wrong types | --- ## 4. Check-in — `POST /api/agent/checkin` Heartbeat + posture report (MFA / NAC / DLP — packages P2-G/H/I). The agent calls this **every 5 minutes**. The server upserts the endpoint record and recomputes its status. **Request** (fields other than `enrollment_id` are optional; booleans may be `true`/`"true"`) ```http POST /api/agent/checkin Authorization: Bearer isecos-agent-lab-key Content-Type: application/json { "enrollment_id": "3f1c2b8e-9a44-4d17-a2f5-6b0c9e8d7a11", "hostname": "ws-hn-014", "ip_address": "10.10.5.24", "os_version": "iSecOS 1.0-alpha6 (MotaX)", "kernel_version": "6.12.0-1-amd64", "uptime_seconds": 86400, "total_memory": 16777216000, "disk_total": 512110190592, "mfa": { "enforced": true, "enrolled_users": ["root", "nva"] }, "nac": { "mode": "on", "authenticated": true }, "dlp": { "usbguard_active": true, "blocked_recent": 2 }, "policy_version_applied": "1" } ``` Posture fields are collected locally by the agent: | Block | How the agent collects it | |---|---| | `mfa.enforced` | `pam_google_authenticator` present in `/etc/pam.d/common-auth` | | `mfa.enrolled_users` | users with a `~/.google_authenticator` file | | `nac.mode` | `/etc/isecos/nac/mode` | | `nac.authenticated` | `wpa_cli status` shows `wpa_state=COMPLETED` on any interface | | `dlp.usbguard_active` | `systemctl is-active usbguard` | | `dlp.blocked_recent` | count of `target.new='block'` in `/var/log/usbguard/usbguard-audit.log` | **Response `200`** ```json { "status": "ok", "endpoint_status": "online", "server_time": "2026-11-06T08:20:31.001122+00:00" } ``` **Status computation (server side)** 1. Start at `online`. 2. `warning` — if `dlp.usbguard_active` is not truthy (DLP daemon down). 3. `noncompliant` — if `mfa.enforced` is truthy **and** `mfa.enrolled_users` is empty (MFA required but nobody enrolled). 4. Endpoints with no successful check-in within the offline window are shown as `offline` by the console. **Errors** | Status | Body | When | |---|---|---| | 401 | `{"detail": "invalid enroll key"}` | bad Bearer token | | 404 | `{"detail": "unknown enrollment_id"}` | endpoint never enrolled (re-run enroll) | | 422 | FastAPI validation detail | missing `enrollment_id` / wrong types | Every check-in writes an `agent_events` row: `event="checkin"`, `detail="mfa=enforce:root,nva nac=on:authed dlp=active:2-block"`. --- ## 5. Policy pull — `GET /api/agent/policy/{enrollment_id}` Returns **all active policies** (at most one per type: `mfa`, `nac`, `dlp`, `cis`). The agent calls this right after every check-in. **Request** ```http GET /api/agent/policy/3f1c2b8e-9a44-4d17-a2f5-6b0c9e8d7a11 Authorization: Bearer isecos-agent-lab-key ``` **Response `200`** ```json { "policies": [ { "name": "MFA bat buoc (P2-G)", "type": "mfa", "config": { "enforce": true }, "version": "1" }, { "name": "NAC enforce default-deny (P2-H)", "type": "nac", "config": { "enforce_mode": "on" }, "version": "1" }, { "name": "DLP chan USB storage (P2-I)", "type": "dlp", "config": { "usb_storage": "deny" }, "version": "1" } ] } ``` **Errors** | Status | Body | When | |---|---|---| | 401 | `{"detail": "invalid enroll key"}` | bad Bearer token | | 404 | `{"detail": "unknown enrollment_id"}` | endpoint not enrolled | Each pull writes `agent_events` row `event="policy-pull"` with `detail=" active"`. --- ## 6. Policy apply (agent side, local) The agent applies pulled policies with the local `isecos-*` CLI tools — it never receives shell commands from the server, only declarative JSON: | Policy | Config key | Agent action (local command) | |---|---|---| | `nac` | `config.enforce_mode = "on" \| "off"` | `isecos-nac enforce on` / `isecos-nac enforce off` | | `dlp` | `config.usb_storage = "deny"` | `isecos-dlp deny-all-storage` | | `mfa` | `config.enforce = true` | **Report-only.** Enrollment stays an operator procedure (`isecos-mfa enroll`); if no user is enrolled, the agent logs a warning via `logger -t isecos-agent` | Applied changes are recorded in the local syslog: `journalctl -t isecos-agent` (e.g. `applied: nac=on dlp=deny policy-ok`). ## 7. Events & audit trail Every agent interaction is persisted server-side in the **`agent_events`** table: | Column | Values | |---|---| | `event` | `enroll` · `checkin` · `policy-pull` · (`apply`, `error` reserved) | | `enrollment_id` | the endpoint's UUID | | `detail` | compact posture / policy summary | | `created_at` | UTC timestamp | The portal **AuditLog** view (`/portal/audit`, redirected from `/logs/`) reads this table, so administrators see enrollments, check-ins, and policy pulls for the whole fleet. The full Wazuh-backed log store is planned for Phase 3 (see `docs/server-infrastructure.md`). ## 8. Rate limiting & operational notes - **No application-level rate limit** is implemented in the MVP. Protection layers: - Cloudflare (tunnel edge) in front of `isecos.isecurity.vn` — enable WAF/rate rules there; - the natural cadence of one call set per endpoint per 5 minutes; - a future Redis-backed limiter is specified in `docs/control-center-spec.md` (Redis table `rate_limit`). - Agent HTTP timeout: `--max-time 10` seconds; failures are **non-fatal** (agent exits 0 on check-in failure, exits 1 only when enroll fails) — a temporarily unreachable CC never disturbs the endpoint, and policy state persists locally between pulls. - Agents are *pull-only*: the CC never opens connections to endpoints (works behind NAT). - Clock: all timestamps are UTC ISO-8601 (`server_time`); endpoints should run `chrony`. - To re-enroll a machine: delete `/etc/isecos/cc-agent.id` and restart `isecos-agent.service`. The old endpoint row remains in the console (dedupe by hostname). - Health of the CC itself: `GET /api/health`, `GET /api/health/ready` (503 when DB/disk unhealthy), `GET /api/health/metrics` (Prometheus). ## 9. Quick end-to-end test ```sh KEY=isecos-agent-lab-key BASE=https://isecos.isecurity.vn/api # lab: http://10.0.2.2:8000/api # Enroll EID=$(curl -s -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{"hostname":"test-01","os_version":"iSecOS 1.0-alpha6"}' \ $BASE/agent/enroll | grep -o '"enrollment_id": *"[^"]*"' | cut -d'"' -f4) echo "EID=$EID" # Check-in curl -s -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d "{\"enrollment_id\":\"$EID\",\"hostname\":\"test-01\", \"mfa\":{\"enforced\":false,\"enrolled_users\":[]}, \"nac\":{\"mode\":\"on\",\"authenticated\":true}, \"dlp\":{\"usbguard_active\":true,\"blocked_recent\":0}}" \ $BASE/agent/checkin # Pull policies curl -s -H "Authorization: Bearer $KEY" $BASE/agent/policy/$EID ``` On the endpoint itself: `isecos-agent once` (one cycle), `isecos-agent status` (shows EID, CC URL, posture), `systemctl list-timers | grep isecos-agent`.