Security
This document is for security and platform-engineering reviewers evaluating Milieu for production. It states the security model, the controls that enforce it, and (explicitly) the current limitations and non-goals. Where a control is planned but not yet shipped, this document says so and links to ROADMAP. Don’t assume a planned control is in place.
Contents
- Security model & trust boundaries
- Sandboxing & command isolation
- Network egress control
- Identity & PKI
- Authentication & authorisation
- Secrets management & the gateway
- Auditability & tamper-evidence
- Data isolation & multi-tenancy
- Supply chain & build integrity
- Production hardening checklist
- Reporting a vulnerability
- Known limitations & non-goals
Security model & trust boundaries
The central design assumption is that an agent may be compromised, by a prompt injection, a poisoned knowledge document, or a buggy skill. The architecture limits the blast radius of a compromised agent. It doesn’t try to prevent compromise. An agent runs with least privilege and crosses three explicit, independently audited trust boundaries:
| Boundary | Who is on each side | Control | Audit |
|---|---|---|---|
| Sandbox | the agent’s code/tools vs the host | OS sandbox + permission allowlist + egress allowlist + fixed PATH | var/audit/audit.jsonl |
| API edge | external programs vs Milieu | one-way TLS + bearer API key → one agent, interaction-only | var/api/audit.jsonl |
| A2A edge | outside agents vs Milieu | bearer API key → one agent, which must be the agent addressed | var/api/audit.jsonl (shared: same credential) |
| Built-in issuer | a person signing in vs Milieu | password (argon2id) for a host-created person, PKCE-bound codes, and ES256 tokens for the agents granted to them. Five failures lock a name for 15 minutes | var/auth/audit.jsonl |
| MCP edge | a person’s assistant vs Milieu | bearer API key → one agent, or an OAuth access token → the agents its milieu:agent:* scopes name | var/api/audit.jsonl (shared), with the person recorded |
| Admin portal | an operator’s browser vs Milieu | TLS + session cookie from a host-created operator account. Cross-site request forgery (CSRF) + origin checked on every mutation. The portal administers the installation, so a session is as powerful as host access for those operations | var/web/audit.jsonl |
| Gateway | the agent vs high-value secrets | mutual TLS (mTLS). The secret is injected gateway-side, never in the sandbox | var/gateway/audit.jsonl |
These are distinct trust domains with separate certificate authorities (CAs). API clients (programs) and gateway proxies (agents) can’t impersonate one another.
Sandboxing & command isolation
Every command an agent runs goes through a pluggable sandbox
(internal/platform/sandbox):
- Linux / Bubblewrap (
bwrap): explicit--ro-bind/--bind/--unshare-*. The agent home is bound read-only with the scratch directory bound writable over it. - macOS /
sandbox-exec(sbexec). none: no isolation, and never auto-selected. You must setMILIEU_SANDBOX=noneexplicitly (used where the container or pod is the boundary).
Controls that hold regardless of backend:
- Command allowlist. An agent can invoke only the CLI patterns granted to it.
Matching is on argument tokens (
git(log:*)), never on a flattened string. See the permission grammar. - Fixed sandbox PATH. Commands resolve against an operator-set PATH, never the
caller’s
$PATH, so the audited and executed binary is deterministic. Agents can’t change it, and relative entries are ignored. - No ambient secrets. The sandbox gets
PATHandHOME=/agentonly. Host environment variables reach an agent only through anagent.toml [env]reference, and only if the operator allowlisted that name inMILIEU_ENV_PASSTHROUGH, so anagent.tomlauthor can’t pull, for example,ANTHROPIC_API_KEYinto an agent. - Configuration is read-only to the agent it configures. A sandboxed command
can write only to
var/agents/<code>/work/(plus the usual temp directories). The rest of the agent’s home (agent.toml,skills/,personality.md,responsibility.md, the proxy identity) is readable but not writable, because a command that could write there could grant itself a command, widen its own[network] allow, or rewrite its instructions for the next run. The built-in tools write everything an agent legitimately writes (memory, transcripts, sessions) host-side, and those tools are permission-checked and audited. - Operator-authored mounts. Milieu exposes read-only host paths only through
[[mount]]blocks whose source lives outside the agent’s writable home, so an agent can’t self-expose a path by planting a symlink. - Pipeline staging. Each
thenstage is permission-checked and sandboxed independently.
One caveat applies to containers: bwrap needs nested namespaces / CAP_SYS_ADMIN, which
hardened pods drop. The default Kubernetes posture is MILIEU_SANDBOX=none with
the pod as the isolation boundary (one tenant per pod), keeping pods fully
hardened. See DEPLOYMENT → Kubernetes.
Under Podman on a VM, bwrap works without CAP_SYS_ADMIN after /proc is
unmasked and the Linux Security Module (LSM) label is relaxed. The Quadlet units
do that and keep the rest of the container hardened. See DEPLOYMENT → Running under
Quadlet.
Network egress control
Outbound network access is a deny-by-default, per-host allowlist (agent.toml [network] allow). An empty or absent list means no network. Entries are host,
host:port, *:port, or *.
OS sandboxes can’t filter by host, so a restricted policy is enforced by a
per-invocation egress proxy outside the sandbox. The sandbox can reach only the
proxy. The command gets HTTPS_PROXY/HTTP_PROXY, and the proxy authorises
each CONNECT against the allowlist before it dials. Names are resolved by the
proxy, on the host. ["*"] skips the proxy and shares the host network.
- macOS /
sbexec: the proxy listens on the host loopback, and the profile allows outbound connections only to its port. - Linux /
bwrap: the sandbox has its own network namespace, whose loopback is not the host’s. The proxy listens on a Unix socket in a private (0700) per-invocation directory, bound read-only into the sandbox. A small bridge, started inside the sandbox before the command, listens on the namespace’s127.0.0.1and forwards each connection to that socket. The namespace has no other route out. An empty list gets no egress bridge and no network. none: not enforced.
The bridge is the milieu binary, bound read-only at /run/milieu/netbridge,
outside the sandbox PATH. Run from that path, under any name, it does only
one thing: forward the loopback ports it was given to sockets in
/run/milieu/net. An agent could copy the binary and run the full CLI, but
only with the sandbox’s own reach: no state directory, no credentials, and no
network beyond the bridged sockets, as with any program it could write itself.
The proxy carries HTTPS and other CONNECT-tunnelled traffic. Plain HTTP
through the proxy is refused, and a client that ignores the proxy variables
gets no network on either backend.
The gateway broker’s proxy ([gateway]) is reached the same way: on the host
loopback under sbexec, and through the bridge under bwrap, whatever the
[network] policy. MILIEU_GATEWAY_URL points at it, and NO_PROXY keeps
proxy-aware clients from sending broker calls to the egress proxy.
milieu lint notes a restricted policy and what it covers.
Identity & PKI
Identity is bound at each boundary by internal/platform/identity:
- API edge: an ECDSA P-256 CA issues the server’s TLS leaf (subject
alternative names (SANs):
localhost,127.0.0.1, and--host). Clients pinvar/api/ca/ca.crt. - Gateway: an Ed25519 CA (TLS 1.3, mutual TLS). The gateway is its own CA: it issues each proxy a client certificate and trusts any certificate the CA signed, so a fleet scales without per-proxy configuration. The proxy holds only a CA-signed leaf and the pinned CA fingerprint (a single value).
Rotation and revocation
- API keys are revocable immediately (
milieu api key revoke <id>). They’re the primary client credential, so rotate them on a schedule. - API server certificate:
milieu api initis idempotent and doesn’t replace a certificate that’s present, so deletevar/api/server/first. The nextinitre-issues the leaf from the existing CA and clients keep their pin. - Gateway proxy identities: re-issue with
milieu gateway issue. To rotate the gateway CA itself, deletevar/gateway/and re-init, then re-issue every proxy (this is a fleet-wide re-pin, so schedule a maintenance window).
Message signing
Each agent has an Ed25519 signing key in
var/signing/<code>/message.key (0600), made when the agent is created, or on
its first send for an agent created before signing. It sits under var/,
outside the agent’s directory, so no sandbox can read it. Every message is
signed as it is queued, over a canonical encoding of the sender, recipient,
subject, the SHA-256 of the stored body, reply depth, the message answered and
the time it was produced. The router checks the signature against the sender’s
key before delivering:
- A message that fails (edited in the outbox, inserted for an agent with no
key, or claimed by the wrong sender) is refused: it stays in the sender’s
outbox with the reason, isn’t delivered or retried, and gets a
route refusedrow in the agent audit log.milieu route refusedlists them. - An unsigned message (queued before signing) is delivered with a warning
and a
route unsigned-deliveredaudit row, so an upgrade loses nothing. Setmessages.unsigned = "refuse"(MILIEU_UNSIGNED_MESSAGES=refuse) once the upgrade-time queues have drained, and an unsigned message is then refused the same way as a failed one. For the upgrade, see OPERATIONS → Message signing.
This protects delivery against someone who can write an agent’s out.db but
can’t read var/signing/. Signing happens on the Milieu host, so a signature shows
that the host queued the message for that agent. It doesn’t show which model
turn produced it. Audit records aren’t signed per agent.
Authentication & authorisation
| Surface | Authentication | Authorisation |
|---|---|---|
| REST API | bearer key (sk_…) over one-way TLS, stored only as a SHA-256 hash in var/api/keys.db, shown once at mint, and revocable | the key maps to exactly one agent, and every request runs as that agent. Interaction-only: no agent or project management, no command execution |
A2A endpoint (milieu a2a serve) | the same bearer keys as the REST API, from the same var/api/keys.db. TLS when served with --tls, and plain HTTP off loopback only with --insecure-http behind a terminating proxy | the key maps to exactly one agent and can address only that agent. A turn runs as that agent under its session quota. Every request, refused or not, lands in the API access log as --edge a2a |
MCP endpoint (milieu mcp serve) | the same bearer keys, or, with [oauth] issuer set, a JWT access token from that issuer, checked against its published keys (asymmetric algorithms only), its exact issuer, an audience bound to the agent’s endpoint (or [oauth] audience), and a required expiry | a key addresses its own agent only. A token addresses the agents its milieu:agent:<code> scopes name, and its holder continues only the conversations they opened. Refusals answer with RFC 6750 / RFC 9728 challenges, and every request lands in the API access log as --edge mcp, a token’s with the person named |
Built-in issuer (milieu auth serve) | a person’s name and password, checked against an argon2id hash. People exist only if created on the host, and aren’t operators | open client registration grants nothing. A token carries an agent’s scope only if the person is granted that agent, is bound to that agent’s endpoint, and lives 15 minutes. Refresh tokens rotate, a replay ends the sign-in’s tokens, and grants are rechecked on every refresh. The sign-in page can’t be framed and refuses cross-site posts |
| Gateway | mutual TLS. The gateway authenticates the proxy by its client certificate | the certificate’s common name (CN) names the agent (proxy-<agent>), and a route admits only the agents it lists (agents, or ["*"], and none listed admits none). Within a route, an explicit allowlist applies (REST upstreams / CLI command patterns / named SQL queries / MCP tools) |
Admin portal (milieu web serve) | session cookie (HttpOnly, Secure, SameSite=Strict) issued after an argon2id password check. Operators exist only if created on the host. Sessions expire on a 30-minute idle and a 12-hour absolute deadline | an operator sees and administers the installation itself, not one agent, within their role (admin everything, chat pages and chat, viewer pages only), set only on the host. Every mutation carries a per-session CSRF token and a same-origin check |
| Sandboxed CLI | caller identity from --as / MILIEU_AGENT | the agent’s permission allowlist. Within a skill run, also the skill’s allowed-tools, for every call the model or a code script makes. The auditor role can’t send messages |
There’s no master key. milieu api key new and milieu apply mint API keys,
and both write var/api/keys.db on the host, so the authority to mint is
write access to that file: the OS account that owns MILIEU_HOME. Milieu
creates its databases 0600 under a 0700 directory (and tightens an existing
one on open), so run the daemon as a dedicated user and keep MILIEU_HOME
owned by it. A second bearer secret guarding the same file would add no
boundary, because anyone able to present it could write the table directly.
The admin portal is the one surface that reaches this authority over a network, and it’s built so that it doesn’t widen it:
- Operators are created only on the host.
milieu web admin addis the only way to make an account, so compromising a session can’t mint a second way in that outlives revoking the first. - It refuses to start unprotected. With no operator account,
milieu web serveexits. Over a non-loopback address it requires TLS. - It’s a different principal from an API key. Portal sessions live in
var/web/admin.dbunder their own CA. Ansk_…key can’t reach a portal route, and a session isn’t a credential any agent holds. - A session is short-lived and revocable. Removing an operator ends their live sessions in the same transaction.
A portal session administers the installation: create
agents, grant and revoke their command permissions, mint and revoke API keys,
create projects and change their membership, and (through Chat) cause an
agent to run. A stolen session is therefore equivalent to shell access to
MILIEU_HOME for those operations. For this reason, the session is
short-lived, revocable, and bound by CSRF and origin, and operators are created
only on the host.
Limits on that authority:
- A minted key is shown once, in the response that mints it. It’s never
redirected through a query string, never logged, and stored only as a SHA-256
hash, the same rule
milieu api key newfollows. - The gateway is read-only from the portal. The portal can see the broker’s CA fingerprint and issued identities and nothing else. The gateway holds secrets in a different trust boundary. A portal that could edit its routes from outside that boundary would be a way around it.
- Knowledge is read-only. Documents are authored as files under version control, and a portal that edited them would be a second, unreviewed path in.
- Chat grants nothing. A turn runs with the agent’s own permissions, sandbox, and egress policy. The operator is recorded as the session’s principal for provenance only. The record confers no authorisation.
Every change lands in var/web/audit.jsonl with the operator, the action, the
target, and the client address, hash-chained like the agent log. The Audit
screen verifies both chains and shows any break as an error, with the reason. A write that can’t be
logged is still performed (an operator locked out of revoking a key during an
incident is the worse failure), and the failure is reported to the server’s log.
Roles (agent.toml): administrator, associate, assistant, and auditor.
Auditors are read-only (reply attempts are rejected at message send, and a
consult is refused), so you can safely grant them broad inspection access.
Consulting is granted per target (consult(<agent>:*)) and is one level deep:
an agent being consulted can’t consult another, so a skill can’t start a chain
or a loop of agents asking each other.
Policies and approvals
Access decisions also go to a Cedar policy
engine, with Milieu’s built-in rules written as Cedar policies and yours in
etc/policies/*.cedar. It covers commands and built-in tools, consulting,
messaging, project access, a credential reaching an agent, conversations, the
admin portal, and on the gateway route admission and what an agent does within
a route. A role is an entity a policy can name (Role::"associate",
OperatorRole::"viewer"), so rules by role go here.
- Shadow mode (
policy.mode = "shadow"): Milieu’s own checks decide, and every disagreement with the policies is logged. Use it to try a policy before it decides anything. - Enforce mode (
policy.mode = "enforce"): the policies decide. It fails closed: a policy that fails to evaluate denies, and a file that doesn’t parse refuses everything the policies decide until it is fixed. - Approvals: a
permitannotated@approval("operator")allows what it matches only after a person approves it (milieu approval, the portal, or a webhook). Each grant lets one action through.
A permit widens access beyond Milieu’s checks, so milieu policy lint warns
about every one. See OPERATIONS → Policies.
Secrets management & the gateway
A secret can reach a tool in two ways, in increasing order of safety:
[env]passthrough: the secret rides in the agent’s sandbox environment, bounded byMILIEU_ENV_PASSTHROUGH(operator allowlist) and[network]. The secret is inside the sandbox, so this way is suitable for low-value tokens.- The gateway: the secret stays in the trusted zone and is injected
gateway-side, so it never enters the sandbox. The agent calls a local proxy
(confined to its loopback port), and the proxy re-originates over mutual TLS.
MILIEU_GATEWAY_URLcarries a per-run token so another local process can’t use the port. A compromised agent can ask the gateway to act for it, but it can’t read the credential.
Gateway hardening properties:
- Secrets are named environment variables resolved at gateway startup. They’re never written to disk and never in the manifest.
- A
cliroute runs with a curated environment (onlyPATH,HOME, and the route’s declared variables), so one route can’t read another’s secrets. - SQL routes accept a named, parameterised query and its parameters, never raw SQL, and never the data source name (DSN).
- The gateway records every brokered call (agent CN, route) in its own hash-chained log, never the secret.
milieu apply never stores generated secrets in the manifest: it mints API
keys and CA fingerprints and reports them once (to stdout or --secrets-out <file>).
Keep that file out of version control. On cloud VMs, instance metadata isn’t a
secret store. Fetch ANTHROPIC_API_KEY from a real secret manager (AWS
SSM/Secrets Manager, GCP Secret Manager). For more information, see
DEPLOYMENT → Artifacts &
secrets.
Auditability & tamper-evidence
Every CLI invocation and built-in tool call appends one row to an append-only,
hash-chained log (internal/domain/audit). Each record’s hash covers its
prev_hash, seq, and payload, so editing, reordering, inserting, or deleting
any record breaks every following hash. milieu audit verify recomputes the
chain and exits nonzero at the first break. Run it in CI against a copy of the
log.
- Appends take an exclusive file lock and read the current tail under it, so a sandboxed CLI run and the daemon extend one chain (no fork).
- The REST API and A2A edges share one access log, and the gateway keeps its
own. Both are hash-chained and verified with
milieu api audit verifyandmilieu gateway audit verify. The portal’s log is verified on its Audit screen. All four chains share one implementation (internal/platform/auditchain), so a verifier can’t drift from the writer. - Record fields: REFERENCE → Audit records.
Keyed chain
The hash chain alone is unkeyed: anyone who can write a log
can also recompute it end to end and leave a chain that verifies. Give the
installation an operator-held key and every new record also carries a mac,
an HMAC-SHA256 of its hash under that key. Someone who can write the log but
can’t read the key can no longer produce records that verify.
- The key comes from
MILIEU_AUDIT_KEY(hex or base64, 32 bytes or more) or from a file named byaudit.key_file/MILIEU_AUDIT_KEY_FILE. Milieu refuses a key file insideMILIEU_HOMEor one that group or other can read: a key beside the log it protects adds nothing. Setting both is an error. - One key covers all five logs (
var/audit,var/api,var/gateway,var/web,var/auth). Each process that writes one needs it, including the gateway host. - The
macis an extra field. Thehashis computed the same way as before, so old logs still verify, and a log keyed part-way has an unkeyed prefix followed by keyed records. - No key, keyed log: a writer without the key keeps appending unkeyed
records, so nothing stops working, and says once on stderr that the log holds
keyed records. A key that disappears is suspicious, and
verifyreports any unkeyed record after a keyed one as a downgrade, with or without the key. - A refused key (bad encoding, too short, a key file in the wrong place or with loose permissions) makes every append to and verify of a keyed log fail with the reason. Milieu doesn’t fall back to writing unkeyed records.
- With the key,
verifychecks every MAC and reports how many records are keyed and from whichseq. Without it,verifychecks the hash chain as before and says the MACs were not checked.
Neither the chain nor the MAC detects tail truncation (lopping
records off the end leaves a shorter-but-valid chain), and an attacker who
rewrites the whole log as unkeyed records leaves a chain that verifies with
0 keyed. Record the seq that keying starts from, and periodically anchor the
latest hash somewhere outside the log (a separate store, a notary, a
security information and event management (SIEM) system).
Data isolation & multi-tenancy
- Per-tenant root. All state resolves from
MILIEU_HOME. A tenant is a separateMILIEU_HOME(separate VM or pod), so agents, knowledge, memory, and audit log are fully isolated. Hosts share only what you configure them to share (an embedding server). See DEPLOYMENT. - Membership gating. A project’s knowledge and memory layers are reachable
only by roster members, and a non-member’s
--inrequest is rejected. - Blob store scope.
var/blobs/is content-addressed (SHA-256) and shared by digest across agents: possessing a digest implies already possessing the bytes, so a digest isn’t a capability to someone who lacks the content. There’s no per-agent access control list (ACL) on raw blobs. Membership gating applies to the knowledge and memory layers only. Treat the blob store as shared within oneMILIEU_HOME. - Storage constraint. Put
var/on local disk, never on NFS: SQLite write-ahead logging (WAL) needs proper POSIX locks.
Supply chain & build integrity
Static, CGO-free binaries. Built with
CGO_ENABLED=0and pure-Go SQLite (modernc.org/sqlite), so the artifacts are statically linked and portable, with no system-library linkage to track.Minimal, pinned dependencies. No external web framework (standard library
net/httponly). Dependencies are pinned ingo.mod/go.sumand are few (TOML, YAML,gojq, SQLite, and an optional PostgreSQL driver).Pinned container image. The repo-root
Containerfileis a multi-stage build (Go → Debian slim) producing a non-root image (fixed UID 10001). Its base images, and the third-party images underdeploy/k8s, are pinned by digest, with the tag each stands for in a comment.deploy/image-digestssays which pins are behind their tag.Scanned, signed, attested image. The image workflow (
.github/workflows/image.yml, run by hand for now) builds the image, writes an SPDX SBOM of it, and scans that before anything is pushed: a high or critical vulnerability with a fix available fails the build. The pushed image is signed keyless with cosign, and carries a build-provenance attestation and an SBOM attestation, all bound to its digest and to the workflow’s GitHub OIDC identity, so there is no signing key to keep. Verify an image before deploying it:cosign verify ghcr.io/geordee/milieu@sha256:<digest> \ --certificate-identity-regexp '^https://github.com/geordee/milieu/\.github/workflows/image\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com gh attestation verify oci://ghcr.io/geordee/milieu@sha256:<digest> --owner geordeeReachable vulnerabilities.
make vulnrunsgovulncheck, which reports the known vulnerabilities the code can reach in its modules and in the standard library of the Go it is built with. CI runs it against the newest Go 1.26, which the image is built with, andmake releaserefuses to start while it finds any.Pinned workflow actions. Every GitHub Action is pinned by commit SHA, with its release in a comment, because a tag can be moved to other code. Dependabot proposes updates to the actions and the Go modules once a month.
Deterministic execution surface. The fixed sandbox PATH means the binary an agent executes for a given command name is fixed at build or deploy time. The environment at run time can’t change it.
Production hardening checklist
-
MILIEU_SANDBOXisbwrap/sbexec(notnone), or the container or pod is the documented isolation boundary. - Each agent’s
[network] allowis the minimal host:port set, with no stray*. - High-value secrets go through the gateway, never
[env]passthrough. -
MILIEU_ENV_PASSTHROUGHlists only the environment variables that must be passed. - The gateway runs in a separate trust boundary (its own VM or namespace),
never co-tenanted with an
apphost. - The API is served over TLS, keys are scoped one per consumer and rotated, and unused keys are revoked.
- The admin portal (
milieu web serve) binds loopback or sits behind TLS with a trusted certificate. Operator accounts are per person, and accounts of operators who leave are removed (which ends their sessions). - The portal’s own log is reviewed alongside the agent log:
milieu webactions don’t appear invar/audit/audit.jsonl, and a key minted through the browser is recorded only invar/web/audit.jsonl. -
ANTHROPIC_API_KEYand route secrets come from a real secret manager and are never in VM metadata or version control. - The image deployed is named by digest, and its signature and provenance
are verified (
cosign verify,gh attestation verify) before it runs. - Your
etc/policies/*.cedarpassmilieu policy lint, and ran in shadow mode with only the disagreements you meant beforepolicy.mode = "enforce". - An audit key is set (
MILIEU_AUDIT_KEYfrom a secret manager, or a0600key file outsideMILIEU_HOME) on every host that writes a log. -
milieu audit verifyruns in CI/cron with the key. The latesthashand theseqkeying starts from are anchored externally (tail-truncation defence). - The daemon runs as a dedicated user, and
MILIEU_HOMEis0700owned by it (write access tovar/api/keys.dbis the authority to mint an API key). -
var/is on local disk with restrictive permissions, and backups covervar/{agents,projects,global,audit}(indices are derived). -
--secrets-outfiles frommilieu applyare stored securely and kept out of version control. -
milieu lintpasses for every agent (no ungranted tools, valid config). -
messages.unsignedis set torefuseafter upgrade-time queues have drained (the default,deliver, is for the upgrade), andmilieu route refusedis empty or explained. - SSH / management ports are CIDR-restricted. Only the gateway (and a shared embedder) accept inbound. See DEPLOYMENT → Networking & ports.
- Container base image pinned by digest and scanned, and pods run non-root with
drop: [ALL].
Reporting a vulnerability
Report suspected vulnerabilities privately to the maintainers. Don’t open a
public issue. Include the affected version or commit, a description, and
reproduction steps. The maintainers aim to acknowledge reports promptly and
coordinate a fix and disclosure. (Maintainers: replace this paragraph with
your security contact (for example, a security@ address or a GitHub private
security advisory link) before publishing.)
Known limitations & non-goals
Current limitations (for status, see ROADMAP):
- Audit records aren’t signed per agent: messages are signed by their sender and checked at delivery, but audit records rest on the (optionally keyed) hash chain and boundary identity. A signing key is read from disk with no rotation command. Replacing one makes the agent’s queued messages fail.
- Per-host egress covers proxy-aware HTTPS only: plain HTTP, and clients
that ignore
HTTPS_PROXY, get no network under a restricted policy. - Audit tail truncation isn’t self-detected, even with the audit key: it needs an external anchor. Without the key the chain is unkeyed, and anyone who can write a log can recompute it.
- The built-in issuer’s signing key doesn’t rotate: replacing it invalidates every outstanding token. Its sign-in limit is per name, in memory, and per process, so a restart forgets it, and someone who knows a person’s name can lock them out for 15 minutes. Open registration isn’t rate-limited.
- Blob store has no ACL and no garbage collection: it’s shared by digest
within a
MILIEU_HOMEand grows unbounded with attachments. bwrapis unavailable in hardened pods: Kubernetes uses pod-as-boundary. Quadlet VMs keep it (DEPLOYMENT §1.2).- An agent with unrestricted network can reach the cloud metadata server on
a GCP VM, and with it the VM’s service account: its image pull, the Anthropic
secret or Vertex AI, whichever that deployment grants. A restricted policy
can’t, unless an entry matches the metadata server (its address, or
*:80).
Non-goals (left to the operator or platform):
- TLS termination / ACME, DNS, load balancing, autoscaling, and backups of
var/are out of scope for the bundled deploy configs. For more information, see DEPLOYMENT → Out of scope. - Milieu doesn’t attempt to prevent prompt injection or model misbehaviour. It contains a misbehaving agent through the preceding boundaries.