Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Permissions & Security

A build permission is the security building block that ties the public build server to the private build machine. Without permissions, a build machine would have to trust the build server absolutely; with permissions, the build machine verifies every incoming session locally and can refuse anything that doesn’t match its own embedded public key.

What’s in a build permission

#![allow(unused)]
fn main() {
pub struct JobGrant {
    pub user_id: UserId,
    pub job_id: JobId,
    pub issuer_id: IssuerId,
    pub issued_at: u64,           // unix timestamp
    pub ttl_secs: u32,            // 5..=30 for permission tokens
    pub execution_params: ExecutionParams,
    pub webrtc: WebRtcPermissions,
}

pub struct WebRtcPermissions {
    pub allowed_channels: Vec<String>,    // e.g. ["espctl", "pty", "firmware"]
    pub max_bandwidth_kbps: u32,          // sliding-window enforced
    pub max_message_rate: u32,            // messages per second
    pub ice_servers: Vec<IceServer>,
    pub peer_fingerprint: String,         // SHA-256 of the requester's certificate
}
}

The whole struct gets encoded and signed with a digital signature by the build server. The build machine embeds the matching public key at compile time and verifies the signature locally before honoring any session.

Lifecycle

1. Browser/MCP client computes its certificate fingerprint
   (SHA-256 of the cert in DER form).
2. Client POSTs /grant/request with the fingerprint and required channels.
3. Build server:
   - Authenticates the requester (session/JWT/MCP_AUTH_SECRET).
   - Checks rate limits and quota.
   - Picks ICE servers (direct and fallback relay with rotating credentials).
   - Builds a JobGrant struct with TTL <= 30s.
   - Signs it with the issuer's private key.
4. Client receives the signed permission + ICE servers + ephemeral job_id.
5. Client opens its WebRTC peer connection using the ICE servers.
6. Build machine (which is checking regularly at /agents/jobs) sees the new
   job and the permission.
7. Build machine verifies:
   - The permission signature against the embedded public key.
   - issued_at + ttl_secs > now.
   - The peer certificate fingerprint matches
     permission.webrtc.peer_fingerprint (this happens after ICE completes,
     when the encrypted transport hands over the cert).
8. Build machine honors only channels in permission.allowed_channels.
9. Build machine enforces max_bandwidth_kbps and max_message_rate per
   channel.
10. When the build finishes (or the permission expires), the build machine
    tears down.

What the security model assumes – and doesn’t

Assumes:

  • The build machine’s embedded public key has not been swapped out by an attacker who already has root on the build machine host (if they do, the game is over anyway).
  • The build server’s private key is kept on the build server host and not leaked. If it leaks, an attacker can mint permissions for anyone, but they still can’t make the build machine run arbitrary code beyond what the sandbox permits.
  • The browser’s certificate fingerprint is unique to the session; it’s computed fresh on each peer-connection.

Does not assume:

  • That the build server is trusted to read or modify build contents. It cannot – the data channels are end-to-end encrypted with the build machine.
  • That the network between client and build machine is trusted. WebRTC through a fallback relay encrypts the entire payload; intermediaries see only wrapped ciphertext.
  • That CORS or CSP alone are sufficient browser-side protections. The fingerprint binding makes a stolen permission useless to anyone whose certificate doesn’t match.

Channel whitelist enforcement

The most concrete security property the build machine provides is the channel whitelist: a build permission lists exactly which WebRTC data channel names are allowed (e.g. ["espctl", "pty", "firmware"]), and the build machine rejects any channel opened with a different name.

This is enforced in the build machine’s on_data_channel handler – before any message is read, the channel is closed if its label isn’t in the whitelist. There is no server-side opt-out and no per-message override.

If a future build adds a new data channel (say, a metrics channel), every client that needs it will need to request the new name in required_channels. The build server will refuse to issue permissions for unknown channel names that aren’t in the operator’s allowed-list.

Bandwidth and rate limiting

Each build permission carries max_bandwidth_kbps and max_message_rate numbers. The build machine enforces both with a sliding window:

  • Bandwidth: byte counter over the trailing 1-second window. When the rolling sum exceeds the cap, writes slow down until the window slides forward.
  • Message rate: message counter over the trailing 1-second window. Same enforcement model.

These are per-channel, not per-session. The firmware channel typically gets a much larger bandwidth budget than the espctl channel.

Permission expiry – short by design

The default permission TTL is 5-30 seconds. This is short on purpose:

  • An attacker who somehow steals a permission has only a few seconds to use it.
  • Compromise of a build server key has limited blast radius – the attacker can mint permissions going forward, but they can’t replay permissions from yesterday.
  • Long-running build sessions (e.g., interactive serial monitoring) get separate, longer-lived “session permissions” with a TTL up to the operator’s configured maximum (typically a few minutes). The user requests the longer session via the timeout_secs field in the permission request.

For interactive PTY sessions, the typical pattern is to refresh the permission periodically by re-issuing a new one mid-session. The client and build machine hand off the new permission on the espctl channel without dropping the underlying peer connection. The user doesn’t need to do anything — the client handles refresh automatically.

Operational checklist

If you’re operating a build server:

  • Signing key custody. Generate the key with aegis-keygen, store the private key in a secrets manager (or at minimum, /etc/aegis/secrets.env with mode 600), and never check it into a repo.
  • Public key distribution. The matching public key must be embedded in the build machine binaries you ship. The build script handles this – you set the trusted public key at compile time and the build machine verifies against it.
  • Relay credential rotation. Fallback relay credentials are rotated per-session by the build server; you don’t need to manage them manually.
  • CORS pinning. The build server only accepts /grant/request from origins listed in ALLOWED_ORIGINS. Set this to your exact production origin (e.g. https://esphome.cloud); never use *.

See also