Security & Permissions
Section titled “Security & Permissions”Traditional infrastructure secures a dozen protocols, each with its own authentication model, encryption scheme, and vulnerability surface: SSH keys scattered across machines, VPN configs shared over Slack, database passwords in environment variables. Every protocol is another entry point to secure.
Hoody collapses this surface to one protocol (HTTPS, over HTTP/2 and, where a server has it turned on, HTTP/3), one gateway (the proxy), and one enforcement point. Instead of securing 19 different services, you secure one proxy; instead of managing 6 different authentication mechanisms, you configure one permission layer. Certificate issuance and renewal need no configuration from you, and every URL is served over HTTPS.
Security is structural here rather than added afterwards: workloads run on a machine allocated to you, and isolation applies to every process and container.
Layer 1: Cryptographic URL unguessability
Section titled “Layer 1: Cryptographic URL unguessability”The first layer of security is not a password or a token. Every container ID is 24 hexadecimal characters: 96 bits of entropy, the same keyspace as a strong encryption key.
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.com └──────────┬──────────┘ 24 hex chars = 2^96The numbers:
- 2^96 is 79,228,162,514,264,337,593,543,950,336 possible container IDs
- At 1 billion guesses per second, enumeration takes 2.5 × 10^12 years
- The universe is 1.38 × 10^10 years old, so a full scan would run for about 180 times its age
Container URLs cannot be scanned, enumerated, or guessed. There is no directory listing and no discovery endpoint; if you do not know the URL, you cannot reach the resource.
This is what makes “open by default” workable: with no permissions configured, the unguessable URL is the secret, and withholding it denies access. Once permissions are configured, knowing the URL is no longer authorization: sharing the URL shares the address rather than the access. The security model starts at the URL, before any authentication layer runs.
Layer 2: Container isolation
Section titled “Layer 2: Container isolation”Each container’s boundary is enforced by the kernel, not by convention or configuration.
Filesystem isolation
Section titled “Filesystem isolation”Each container has its own root filesystem, with no shared volumes by default. Container A cannot read container B’s /etc/passwd, cannot write to container B’s /home, and cannot even know container B exists on the same server.
Network isolation
Section titled “Network isolation”Each container has its own network namespace and routing table. Each gets its own Linux bridge on a dedicated /30 subnet, so containers never share a bridge and there is no shared internal segment to eavesdrop on. A container has a private IPv4 there, masqueraded behind the host’s public address; it does not have a dedicated public IPv4. Containers reach each other the same way any two machines on the internet do.
Process isolation
Section titled “Process isolation”PID namespaces mean each container sees only its own processes. A compromised container cannot enumerate, signal, or attach to processes in any other container.
Enforcement mechanisms
Section titled “Enforcement mechanisms”| Technology | What it does |
|---|---|
| Linux namespaces | Isolate PIDs, network, mounts, users, IPC |
| seccomp | Syscall filtering with additional targeted restrictions, layered on the container runtime’s default profile |
| Hardened kernel | A custom hardened Hoody kernel, patched and locked down to reduce attack surface |
| Hardened container runtime | System containers on the Hoody kernel, with optional dedicated VM instances for full kernel isolation |
| No shared kernel memory | Containers cannot read each other’s RAM |
Container isolation limits a compromised container’s direct access to other containers’ files and processes. It can still affect shared storage mounts and the services its credentials or network access reach, and replacing the container does not undo those effects: delete it, restore a clean snapshot, and check what it could reach. No isolation boundary is absolute (kernel and runtime vulnerabilities exist), but the unit of compromise stays small.
Layer 3: Bare metal ownership
Section titled “Layer 3: Bare metal ownership”Most cloud platforms run your workloads on shared hardware: your containers share a hypervisor with strangers’ containers, and your memory shares physical DIMMs with unknown processes. Spectre, Meltdown, and their variants demonstrated that CPU-level side-channel attacks can leak data across hypervisor boundaries.
On a rented or owned Hoody server, containers run on a physical machine allocated to you alone. There is no shared hypervisor and no other customer’s workload on the hardware.
The security implications of a dedicated machine:
- No cross-tenant side channels. Nobody else’s workload shares your CPU cache or memory bus, which removes the entire class of attacks that depends on a neighbouring tenant.
- No hypervisor escape risk. There is no shared hypervisor to escape from; your containers run on bare Linux. (If you opt in to running your own VMs, that hypervisor is yours, on your own machine, not a boundary shared with other tenants.)
- Physical control. You control the server and its network configuration, and nobody else rents the host.
- Performance predictability. All CPU cycles, memory, and disk IOPS are yours, so strangers’ workloads cannot slow yours down.
The free tier is not this
Section titled “The free tier is not this”Every guarantee above describes a machine that is yours. A free server is not a machine; it is a slice of one. Free-tier servers are capped slices carved out of a shared physical host that also carries other people’s slices. Everything in Layers 1, 2, and 4 through 8 still applies to your containers. This layer does not.
| Free slice (shared host) | Rented or owned machine | |
|---|---|---|
| Hardware | Shared with other tenants | Yours alone |
| Kernel | One kernel, shared with strangers | One kernel, shared only with yourself |
| CPU cache / memory bus | Shared; the Spectre/Meltdown class of cross-tenant side channel is not ruled out | Not shared with anyone |
| Disk blocks | Deduplicated host-wide, so identical blocks may be physically shared across tenants | Deduplicated only among your own containers |
| Noisy neighbors | Possible | None |
# List your server entries: dedicated rentals plus any free-tier sliceshoody servers list
# A rented or owned server runs its own proxy and containers, with no# infrastructure shared with other customers; a free-tier entry is a slice# of a shared hostimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// List your server entries (dedicated rentals and free-tier slices)const servers = await client.api.servers.list();
// Each rental includes rental dates/status and nested server hardware details; it does not include containers or proxy details// On a dedicated machine no hypervisor or kernel is shared with another// tenant; free-tier slices share their host's kernel# Your serverscurl "https://api.hoody.com/api/v1/servers" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Each rental in the response includes:# - server.name (your proxy node, e.g. node-example-1)# - server.region / server.specs (CPU, RAM, disks, network)# - rental_start / rental_end / statusOne request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists your server entries, both dedicated rentals and free-tier slices, including region, hardware specs, and rental status.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/servers&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Layer 4: Permission system
Section titled “Layer 4: Permission system”When URL unguessability is not enough and you need explicit authentication, the proxy provides a multi-layered permission system.
Authentication methods
Section titled “Authentication methods”| Method | Mechanism | Use case |
|---|---|---|
| Password | HTTP Basic Auth | Quick protection for internal tools, demos |
| JWT | Token with claims validation | API consumers, AI agents, service-to-service |
| IP whitelist | Allow by IP address or CIDR range | Office networks, known servers, CI/CD runners |
| Token | Exact token from a configured header, cookie, or query parameter | Machine-to-machine, webhook endpoints |
Two levels of scope
Section titled “Two levels of scope”Project-level permissions apply to every container in the project:
Project "production" → deny all by default └─ Group "devops": IP 203.0.113.0/24 → allow terminal, files, display └─ Group "monitoring": Token → allow http port 8080Container-level permissions apply alongside project settings for specific containers; when both documents exist, both govern and must be evaluated together:
Container "public-api" → evaluated together with the project document └─ Group "world": IP 0.0.0.0/0 → allow http only └─ Group "operators": JWT → allow everythingService-level granularity
Section titled “Service-level granularity”Permissions are not all-or-nothing. Each authentication group gets fine-grained access per service:
# Build permissions with the granular commands (each PATCHes one field).# Read the current file_version first, then pass it as --if-match file:vN# (a write is rejected 428 without it, 412 if stale). Re-read after each# write, since every mutation bumps the version.V=$(hoody containers proxy permissions get -c $CONTAINER_ID -o json | jq -r '.file_version')
# Create the 'office' IP auth group (only this public range may reach the proxy).# Use the public egress CIDR your clients actually come from; a private range# like 10.0.0.0/8 can never match an internet visitor's source address.hoody containers proxy groups ip set -c $CONTAINER_ID \ --group-name office --range 203.0.113.0/24 --if-match "file:v$V"
# Grant that group per-service access (re-read file_version between writes)V=$(hoody containers proxy permissions get -c $CONTAINER_ID -o json | jq -r '.file_version')hoody containers proxy groups permissions set -c $CONTAINER_ID \ --group-name office --program terminal --access true --if-match "file:v$V"
V=$(hoody containers proxy permissions get -c $CONTAINER_ID -o json | jq -r '.file_version')hoody containers proxy groups permissions set -c $CONTAINER_ID \ --group-name office --program files --access true --if-match "file:v$V"
# Everything else stays deniedV=$(hoody containers proxy permissions get -c $CONTAINER_ID -o json | jq -r '.file_version')hoody containers proxy default set --default deny -c $CONTAINER_ID --if-match "file:v$V"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Granular permissions per service// Read the current `file:v<N>` ETag from proxy.containerPermissions.get() firstawait client.api.proxy.containerPermissions.set(containerId, { project: PROJECT_ID, container: containerId, groups: { humans: { type: 'password', username: 'ops', password: 'secure-pass', salt: 'unique-salt' }, agents: { type: 'token', header: 'X-Agent-Token', value: 'agent-secret-token' } }, permissions: { humans: { terminal: true, files: true, display: false, sqlite: false }, agents: { terminal: true, files: true, exec: true, sqlite: true, browser: true } }, default: 'deny'}, { ifMatch });# Production lockdown: only HTTP traffic from known IPs# If-Match comes from GET .../proxy/permissions (file_version): 428 if absent, 412 if stalecurl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v2" \ -d '{ "project": "'"$PROJECT_ID"'", "container": "'"$CONTAINER_ID"'", "groups": { "office_egress": { "type": "ip", "range": "203.0.113.0/24" }, "operators": { "type": "jwt", "secret": "your-jwt-secret", "algorithm": "HS256", "sources": ["header:Authorization"] } }, "permissions": { "office_egress": { "http": [8080] }, "operators": { "terminal": true, "files": true, "display": true, "sqlite": true, "exec": true } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Replaces the container’s whole permissions document with a production lockdown: HTTP open only to the office CIDR, everything else gated behind a JWT-authenticated operators group. Fetch the current document first and put its ETag in place of file:v2, or the write is rejected.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v2&json={"project":"PROJECT_ID","container":"CONTAINER_ID","groups":{"office_egress":{"type":"ip","range":"203.0.113.0/24"},"operators":{"type":"jwt","secret":"your-jwt-secret","algorithm":"HS256","sources":["header:Authorization"]}},"permissions":{"office_egress":{"http":[8080]},"operators":{"terminal":true,"files":true,"display":true,"sqlite":true,"exec":true}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Source IP Guard
Section titled “Source IP Guard”Hoody Kit programs are reached through their URLs only. A request that does not come through the program’s URL is refused by the Source IP Guard with 403 and the error code forbidden.
Layer 5: Container firewalls
Section titled “Layer 5: Container firewalls”Beyond the proxy permission layer, each container has host-level firewall rules that control network traffic at the packet level.
The rules are configured on the host, not inside the container, so a compromised container cannot modify its own firewall. This is iptables on the bare metal, scoped to the container’s network namespace, not iptables inside a container.
| Rule type | What it controls |
|---|---|
| Ingress | Which IPs/ports can reach the container |
| Egress | Which IPs/ports the container can reach |
| Protocol | TCP, UDP, and ICMPv4 (icmp4) filtering |
| Default stance | Default-allow: everything passes until you add rules, and each rule can allow, reject, or drop what it matches |
You can also install iptables, nftables, or ufw inside the container for defense in depth: two independent layers of network control. One caution: the daemon keeps its per-program sandbox ingress rules in an nftables table of its own, so a tool that flushes or resets nftables removes them too. A program that declares an ingress policy then refuses to launch until the table is back, and a program that is already running is not stopped: it keeps listening without its ingress filtering until the next firewall change or daemon start reinstalls the rules. There is no periodic repair: reinstall the rules with a daemon restart or a successful ingress-policy change, then check GET /api/v1/daemon/programs/{id}/sandbox on the daemon service URL before relying on that filtering again. See Program Sandbox.
Layer 6: Controlled network exit
Section titled “Layer 6: Controlled network exit”By default a container has no dedicated public IPv4: its outbound traffic is masqueraded behind the host’s address, and egress to public IPv4 is permitted. Private and special-use ranges, common SMTP ports (25, 26, 366, and 2525), and tunnelling protocols 4, 41, 47, and 132 are blocked at the host. Configure a network exit when you need every outbound connection to take a specific path.
When a container needs internet access, you configure the exit path at the host level, where the container cannot tamper with it:
- SOCKS5/HTTP/HTTPS proxies as exit nodes
- Commercial VPN endpoints: point a container at any provider’s internet-reachable SOCKS5/HTTPS proxy endpoint with no in-container configuration (native WireGuard routing is planned for a future update)
- Block mode to prevent all outgoing traffic
- Custom DNS servers (up to 4)
Once an exit is configured, direct egress to the internet is removed and the container cannot get around it: the exit path is enforced on the host, outside the container’s reach, so a compromised container cannot add its own route or fall back to a direct connection. Platform services (DNS, the package mirror, the AI gateway) stay reachable on their own paths. Until you configure an exit, treat container egress as open.
Outbound requests from Kit services
Section titled “Outbound requests from Kit services”The rules above govern traffic your own programs open. A Kit service that makes an HTTP request on your behalf is bound separately, and more tightly: Egress when it proxies a request, and Run when it delivers a webhook or fetches a remote package index or source.
A destination one of these services dials itself goes to a globally routable IPv4 address or it does not go at all. Private, loopback, link-local, CGNAT, multicast and reserved destinations are refused, and no flag, environment variable, file or API field admits one. Each name is resolved once and the address that was checked is the address that is dialled, so a short-lived DNS record cannot pass the check and then point elsewhere. IPv6 destinations are refused: container networking is IPv4-only, and these services enforce that themselves rather than assuming it.
One exception exists, and it is a chained exit rather than a destination. Egress accepts a SOCKS5 upstream on the container’s own loopback, which is how hoody egress local start hands traffic to the machine you run it on. Once an upstream is in force, the far end of that chain decides where a request goes, so the rules above describe what this service dials, not what the exit does after it. Two things still hold on this side: an IPv6 literal is refused before the request is dispatched at all, and a socks5 upstream has its target resolved here, where an IPv4 address is selected. The upstream’s own address is checked by the same rules.
Run adds rules of its own. Proxy environment variables in the container do not redirect its fetches. Webhook deliveries and remote index fetches follow no redirects at all, while a source fetch follows at most ten http(s) hops and refuses a hop pointing at a prohibited address. A webhook refused by these rules records one failed attempt rather than burning its whole retry budget on the same verdict.
A refusal is visible rather than silent. Egress answers 403 destination not permitted on the request, and a configured upstream proxy that has no permitted address answers 502 instead of quietly sending the request out directly. See Destination rules for the full behaviour.
Layer 7: Disk encryption
Section titled “Layer 7: Disk encryption”Every Hoody machine runs LUKS full-disk encryption on its bare metal. There is nothing to enable and no way to opt out: the platform provisions encryption when the server is installed, and the orchestrator manages it from there, on free-tier hosts and rented machines alike.
The property that matters is where the key lives: not on the machine it unlocks. The orchestrator holds the key and supplies it out of band at boot, so a reboot comes back up unattended while a drive that leaves the rack does not. A stolen disk or a seized chassis holds ciphertext, and the key that decrypts it stayed behind. That makes theft and physical seizure a hardware loss rather than a data breach.
What it does not do is protect a running host. Once the volume is unlocked, the filesystem is plaintext to anything with host-level access, which includes the platform services that serve your containers and Hoody, which operates the machine. Layer 7 answers the powered-off threat, not the live one. For data that must stay opaque even while the server runs, encrypt above the disk, in your own code, and upload the ciphertext, so the key is yours and never reaches the host in the clear.
Layer 8: Realms
Section titled “Layer 8: Realms”Realms provide API-level isolation. Each realm sees only its own containers:
https://507f1f77bcf86cd7994390aa.api.hoody.com → sees only Realm A's containershttps://507f1f77bcf86cd7994390bb.api.hoody.com → sees only Realm B's containersAuth tokens scope to specific realms, and AI agents in one realm cannot discover, enumerate, or access containers in another realm. This is multi-tenant isolation at the API level rather than network segmentation.
Aliases and public exposure
Section titled “Aliases and public exposure”Proxy aliases put a clean, brandable domain in front of a cryptographic URL. They are also the point where your security model changes.
The cryptographic URL is the secret (Layer 1): the container ID inside it carries 96 bits of entropy and is never meant to be shared verbatim. An alias hides that ID behind a memorable name, which is its job. Hiding the ID is not the same as hiding the surface behind it.
Two failure modes follow you across the alias:
- Metadata leakage. Some programs embed the underlying container ID, internal paths, hostnames, or environment details into HTML, response headers, error pages, or websocket handshakes. The alias hides nothing if the response body says
container_id: 890abcdef…. - Surface exposure. The alias still routes to a specific program. Some programs are user-written code, whose security is your responsibility. Others are privileged control planes where the surface itself is the dangerous action: terminal is a shell, files is a filesystem, sqlite is a database, agent orchestrates everything else.
Programs safe to publish
Section titled “Programs safe to publish”These programs expose HTTP-shaped surfaces that you control. Aliasing them for public sharing, business cards, embedded docs, or customer-facing URLs is the intended use case:
| Program | Why it is safe to publish |
|---|---|
http | Your web server / API. The auth and authorization are your application logic: you decide what is exposed. |
exec (hoody-exec) | Scripts you wrote with explicit handlers and routes. Behaves like any HTTP framework. |
pipe (hoody-pipe) | A relay with no on-disk state, gated by proxy permissions. POST/PUT and GET stream bytes one way; ?live broadcasts to late joiners and ?ws pairs two peers for messages in both directions. The send, receive, share, video and progress pages use the same permission rules. Publishing a name also makes its status observable to permitted callers. |
tunnel (hoody-tunnel) | HTTP and TCP forwarding of a local service through the proxy. Auth runs at the proxy boundary. |
These four are the public diffusion set: they expose plain transport, nothing more, and rely on you to decide what the application returns. Combine them with proxy permissions and you have a clean URL backed by real authentication.
Programs for internal use only
Section titled “Programs for internal use only”Every other Hoody Kit program is an operator surface. Aliasing them is fine for internal use behind IP whitelists or strong auth, but never publish those aliases the way you would publish an API URL:
| Program | Why publishing the alias is dangerous |
|---|---|
terminal | The alias becomes a published shell endpoint. One credential away from arbitrary command execution. |
files | Filesystem browser. Listings, downloads, and uploads against the container’s root. Path leakage is the default behavior. |
sqlite | Live database UI and SQL API. Schema, secrets, and writes, all over HTTP. |
display | Remote desktop with keyboard, mouse, clipboard read and write, and screenshots. Hijacking it hijacks the running session. |
code | Full editor with filesystem access. Extensions can execute code. Reads keys and configs. |
browser | Chromium with JavaScript evaluation, headful on its display by default. Cookies, automation, and credential interception live here. |
agent | The AI agent orchestrates every other service in the container. Compromising the alias compromises everything below it. |
cron, daemon | Scheduled jobs and process control. Inject a job, gain persistent execution. |
curl | HTTP request wrapper. Aliased and exposed, it becomes an open SSRF gateway with your IP and your secrets. |
ssh | SSH over the proxy. Same risk class as terminal. |
For these, prefer the cryptographic URL: the 2^96 keyspace is your authentication of last resort. Recreating a program does not change that URL, because the project and container ids in the hostname stay the same: to cut off a leaked URL, set proxy permissions, or replace the container with one that has a new container id.
Guardrails for a published alias
Section titled “Guardrails for a published alias”For the safe set (http, exec, pipe, tunnel), publishing the alias is the goal. A few guardrails make it more durable:
- Use unique, non-generic names.
acme-billing-apiis not enumerable;api,app,prodare. Generic names collide two ways, both409: scope"server"means another account already took the name on this physical server; scope"account"means your own account already holds it on some server — including this one. - Know what
target_pathandallow_path_overridedo and do not do.target_pathchooses where a bare root request lands (target_path: "/api/v1"sends/to/api/v1). Withallow_path_override: true(the default), every other path passes through as sent. Withfalse, the alias serves only the root, ortarget_pathitself: other paths, sub-paths and assets included, answer404 ALIAS_PATH_PINNED, and a parameter written intarget_pathcannot be overridden, but the visitor’s method, body and other parameters still pass. Neither field limits who can open the link. To limit who can reach the service, scope the container’s proxy permissions. - Apply permissions to the underlying container. Permissions follow the container, not the URL: both the alias and the cryptographic URL inherit them. There is no way to lock down only the alias.
- Watch Certificate Transparency for custom domains. Default container subdomains are covered by a wildcard cert and never appear in CT logs. The moment you CNAME
api.mycompany.comto your alias, that hostname does show up in public CT logs. This is fine for intentional production exposure; just know that custom-domain hostnames are publicly enumerable in a way that*.{serverName}.containers.hoody.comURLs are not. - Delete aliases before deleting containers. Orphaned aliases keep responding (with errors), and stale alias names are an attractive target if reassigned later.
Which address to publish
Section titled “Which address to publish”The container ID is a secret; the alias is a label. Publish the label only for programs whose surface you would also publish. For everything else, the cryptographic URL is the right address.
Security for AI-generated code
Section titled “Security for AI-generated code”AI generates code you cannot fully review. When a human writes code, you can read it. When an LLM generates 10,000 lines in response to a prompt, you cannot meaningfully review every line, every import, or every network call.
That is a consequence of scale, not a failure of discipline. AI-generated code will have bugs and vulnerabilities, and it will make network calls you did not anticipate.
Hoody’s security model is designed for this:
-
Containment. Container isolation keeps a rogue AI-generated process inside its container: it cannot read other containers’ filesystems or signal their processes. Shared storage mounts, credentials and network access you grant remain paths to other data and services, and a writable share stays exposed to destructive code. Reaching the host takes a kernel or runtime vulnerability, the same caveat every isolation boundary carries. Inside the container, a program you run through the daemon can be confined further with a program sandbox: a read-only filesystem, TCP port limits, resource caps and an ingress allowlist. This is optional: the program runs as a daemon program under an existing non-root account with an effective
sandboxblock, and creating a container applies no program confinement by itself. -
Snapshots. Snapshot before an AI makes changes. If it breaks something, restoring takes seconds instead of hours of debugging or a
git bisect. -
Network control. You decide where AI-written code can reach. Configure an exit path and host-level firewall rules, and a container running AI-generated code hits a boundary it cannot modify. Configure nothing and its egress is open, so set the exit before you run code you have not read.
-
HTTP observability. Every HTTP call into your container’s services passes the Hoody proxy: log it, inspect it, rate-limit it, or intercept it with hoody-exec hooks. What those logs contain and how long they are kept is covered in Auditing & Data Collection. Outbound calls your code makes do not pass this proxy; configure a network exit to control their path, and add logging at that exit if you need outbound observability.
The layer stack
Section titled “The layer stack”From bottom to top, each layer narrows the attack surface:
┌─────────────────────────┐│ Application Security │ Your responsibility (input validation, auth logic)├─────────────────────────┤│ Proxy Permissions │ JWT, password, IP, token per service├─────────────────────────┤│ Container Firewall │ Host-level ingress/egress rules├─────────────────────────┤│ Network Control │ No dedicated public IPv4, optional controlled exit├─────────────────────────┤│ Container Isolation │ Namespaces, seccomp, hardened kernel├─────────────────────────┤│ Bare Metal Ownership │ Your hardware, no shared hypervisor (dedicated servers only)├─────────────────────────┤│ Disk Encryption │ LUKS on every host, keys held off the machine├─────────────────────────┤│ Realm Isolation │ API-level multi-tenancy├─────────────────────────┤│ URL Unguessability │ 2^96 keyspace, no enumeration└─────────────────────────┘The layers fail separately (compromising one does not hand an attacker the rest), though they are not fully independent: containers on a host share a kernel. URL unguessability provides passive security even with no permissions configured. Container isolation contains breaches even if the application is compromised. On a dedicated machine, bare metal ownership eliminates entire classes of attacks even if a container is fully taken over. On a free slice that layer is absent, and the stack is one layer shorter.
Practical security postures
Section titled “Practical security postures”Development
Section titled “Development”Permissions: None configuredURL security: Cryptographic (2^96)Firewall: Default allowNetwork: Direct NAT by default
Who can access: Only people who have the URLThis posture suits development, experimentation, and internal tools. With no permissions configured, the unguessable URL is the only gate.
Staging
Section titled “Staging”Permissions: IP whitelist for office/VPNURL security: Cryptographic + IP checkFirewall: drop every source range except the known onesNetwork: Proxied exitThis adds a second factor: even with the URL, you must be on the right network.
Production
Section titled “Production”Permissions: JWT for API, password for operators, IP for infraURL security: Cryptographic + auth requiredFirewall: drop unwanted ports and source ranges, minus what you keepNetwork: No dedicated public IPv4, controlled exitSnapshots: Hourly automatedEvery layer described above is active, and snapshots run hourly.
# Disable the proxyhoody containers proxy disable --container $CONTAINER_ID --if-match "file:v<N>"
# Re-enable when investigation is completehoody containers proxy enable --container $CONTAINER_ID --if-match "file:v<N>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Disable proxy: kill switch; new requests then return 403, and no rule can re-open it// Set it at container level: a container's own `true` overrides a disabled project// Pass the current `file:v<N>` ETag from a prior GET (required: 428 if absent, 412 if stale)let { data } = await client.api.proxy.containerPermissions.get(containerId);let ifMatch = data.etag ?? `file:v${data.file_version}`;await client.api.proxy.containerPermissions.disable(containerId, {}, { ifMatch });// Container keeps running; only proxy reachability is cut
// Re-enable when investigation is complete; every write bumps file_version, so re-read it({ data } = await client.api.proxy.containerPermissions.get(containerId));ifMatch = data.etag ?? `file:v${data.file_version}`;await client.api.proxy.containerPermissions.enable(containerId, {}, { ifMatch });# Disable proxy (kill switch; new requests then return 403)curl -X PATCH "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"enable_proxy": false}'
# The container stays alive, but the proxy now answers 403 instead of forwarding# Re-enable when investigation is completecurl -X PATCH "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"enable_proxy": true}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Disables the proxy for this container, so its service URLs answer 403 instead of forwarding, then re-enables it. Route both links through a different running container’s curl-1 — disabling CONTAINER_ID’s proxy also 403s its own curl service, which would sever the link that undoes it. Replace file:v<N> in each link with the ETag from a prior GET of the permissions document, or the write is rejected.
# Disable
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions/state&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"enable_proxy":false}&response=transparent
# Re-enable
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions/state&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"enable_proxy":true}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
The defaults are open because they are already cryptographically secure. Every additional layer is there when you need it.
Next: Snapshots, point-in-time restore as a security tool.