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_secsfield 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.envwith 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/requestfrom origins listed inALLOWED_ORIGINS. Set this to your exact production origin (e.g.https://esphome.cloud); never use*.
See also
- Build Server & Connection Setup – how permissions are issued.
- WebRTC Build Machine & Data Channels – how permissions are enforced.