Security & Permissions
Section titled “Security & Permissions”Legacy security is a losing game. A dozen protocols, each with its own authentication model, its own encryption scheme, its own vulnerability surface. SSH keys scattered across machines. VPN configs shared over Slack. Database passwords in environment variables. Every protocol is a door. Every door is an attack vector.
Hoody collapses this entire surface to one protocol (HTTPS with HTTP/2 and HTTP/3), one gateway (the proxy), and one enforcement point. You do not secure 18 different services. You secure one proxy. You do not manage 6 different authentication mechanisms. You configure one permission layer. You never configure a certificate — every URL is HTTPS automatically, forever.
This is not a startup that bolted on security after the fact. Hoody has years of privacy-first engineering behind it — built by a team obsessed with the idea that your infrastructure should be yours, running on servers you own, with isolation guarantees that extend to every process, every container, every byte.
Open by default. Bulletproof when ready.
Layer 1: Cryptographic URL Unguessability
Section titled “Layer 1: Cryptographic URL Unguessability”The first layer of security is not a password. It is not a token. It is mathematics.
Every container ID is 24 hexadecimal characters. That is 96 bits of entropy — the same keyspace as a strong encryption key.
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu └──────────┬──────────┘ 24 hex chars = 2^96The math:
- 2^96 = 79,228,162,514,264,337,593,543,950,336 possible container IDs
- At 1 billion guesses per second: 2.5 × 10^12 years to enumerate
- The universe is 1.38 × 10^10 years old
- You would need to brute-force for ~180 times the age of the universe
Container URLs cannot be scanned, cannot be enumerated, and cannot be guessed. There is no directory listing. There is no discovery endpoint. If you do not know the URL, the resource does not exist for you.
This is why “open by default” is not reckless. The URL IS the secret. Sharing the URL IS granting access. Not sharing it IS denying access. The security model starts at the URL, before any authentication layer even runs.
Layer 2: Container Isolation
Section titled “Layer 2: Container Isolation”Every container is a sealed boundary. Not a suggestion. Not a convention. A kernel-enforced perimeter.
Filesystem Isolation
Section titled “Filesystem Isolation”Each container has its own root filesystem. No shared volumes by default. Container A cannot read Container B’s /etc/passwd, cannot write to Container B’s /home, 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 container 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. Each has a private IPv4 there, masqueraded behind the host’s public address; what it does not get is a dedicated public IPv4. Containers reach each other the same way any two machines on the internet do. No internal network to eavesdrop on.
Process Isolation
Section titled “Process Isolation”PID namespaces ensure that each container sees only its own processes. A compromised container cannot enumerate, signal, or attach to processes in any other container.
What Enforces This
Section titled “What Enforces This”This is kernel-level enforcement:
| Technology | What It Does |
|---|---|
| Linux namespaces | Isolate PIDs, network, mounts, users, IPC |
| seccomp | Syscall interception plus targeted denies — notably the btrfs subvolume ioctls, which closes a disk-quota escape — layered on the container runtime’s default profile |
| Hardened kernel | A custom hardened Hoody kernel — patched and locked-down with reduced attack surface |
| Hardened LXC | Container runtime on the Hoody kernel, with optional dedicated VM instances for full kernel isolation |
| No shared kernel memory | Containers cannot read each other’s RAM |
A compromised container stays compromised. It does not spread to your other containers: delete it, restore a clean snapshot, and move on. 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. Your memory shares physical DIMMs with unknown processes.
This is not hypothetical risk. Spectre, Meltdown, and their variants demonstrated that CPU-level side-channel attacks can leak data across hypervisor boundaries. When you share hardware, you share risk.
On a rented or owned Hoody server, your containers run on YOUR bare metal. You hold the whole physical machine. No other customer has containers on it. No shared hypervisor. No neighbor you cannot audit. No “noisy neighbor” performance problems.
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. Your server, your network configuration, and a host nobody else is renting.
- Performance predictability. Every CPU cycle, every memory byte, every disk IOPS is yours. No random slowdowns from strangers’ workloads.
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 subservers: 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 servers -- these are YOUR bare metal machineshoody servers list
# Each server runs its own proxy, its own containers# No shared infrastructure with other customersimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
// Your servers are your infrastructureconst servers = await client.api.serverRental.list();
// Each rental: your hardware (server name, region, specs), your containers, your proxy// No shared hypervisor, no shared kernel with strangers# Your serverscurl "https://api.hoody.icu/api/v1/servers" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Each rental in the response includes:# - server.name (your proxy node, e.g. node-us-nyc-1)# - server.region / server.specs (CPU, RAM, disks, network)# - rental_start / rental_end / statusLayer 4: Permission System
Section titled “Layer 4: Permission System”When URL unguessability is not enough — when 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 |
| Bearer token | Custom token in Authorization header | 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": Bearer token → allow http (read-only)Container-level permissions override project settings for specific containers:
Container "public-api" → override project permissions └─ 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 --default deny -c $CONTAINER_ID --if-match "file:v$V"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
// Granular permissions per service// Read the current `file:v<N>` ETag from proxyPermissionsContainer.get() firstawait client.api.proxyPermissionsContainer.replace(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.icu/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" }'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.
These rules are configured on the HOST, not inside the container. A compromised container cannot modify its own firewall rules. This is not iptables inside a container — this is iptables on the bare metal, scoped to the container’s network namespace.
| Rule Type | What It Controls |
|---|---|
| Ingress | Which IPs/ports can reach the container |
| Egress | Which IPs/ports the container can reach |
| Protocol | TCP, UDP, ICMP filtering |
| Default stance | Default-allow: your rules drop what you name, everything else passes. Add explicit rules to restrict |
Additionally, you can install iptables, nftables, or ufw INSIDE the container for defense-in-depth. Two independent layers of network control.
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 (not tamperable by the container):
- SOCKS5/HTTP/HTTPS proxies as exit nodes
- Commercial VPN endpoints — point a container at any provider’s internet-reachable SOCKS5/HTTPS proxy endpoint with zero in-container config (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.
Layer 7: Disk Encryption
Section titled “Layer 7: Disk Encryption”Every Hoody machine runs LUKS full-disk encryption on its bare metal. There is no switch to flip, no setup step you own, and no way to opt out: the platform provisions encryption when the server is installed and the orchestrator manages it from there. 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 it and supplies it out of band at boot, so a reboot comes back up unattended while a drive that leaves the rack does not. Pull a disk, seize the chassis, walk out with the whole server: what you carry away is ciphertext, and the thing 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 includes Hoody’s own administration of 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: a crypt-wrapped storage backend or application-level encryption, where 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. Different realms are different universes:
https://507f1f77bcf86cd7994390aa.api.hoody.icu → sees only Realm A's containershttps://507f1f77bcf86cd7994390bb.api.hoody.icu → sees only Realm B's containersAuth tokens scope to specific realms. AI agents in one realm cannot discover, enumerate, or access containers in another realm. This is multi-tenant isolation at the API level — not just network segmentation.
Public Exposure: Choose Carefully What You Alias
Section titled “Public Exposure: Choose Carefully What You Alias”Proxy aliases are the bridge from cryptographic URLs to clean, brandable domains. They are also the moment 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 — that is its job. But 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 (your problem). Others are privileged control planes that are the dangerous action — terminal is a shell, files is a filesystem, sqlite is a database, agent orchestrates everything else.
Safe to Alias for Public Diffusion
Section titled “Safe to Alias for Public Diffusion”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 streaming HTTP relay (POST/PUT to send, GET to receive — each path is one-directional, no on-disk state) with permission gating at the proxy. The wire protocol is the only surface. |
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, professional URL backed by real authentication.
Do Not Alias for Public Diffusion
Section titled “Do Not Alias for Public Diffusion”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, and screenshots. Hijacking it hijacks the running session. |
code | Full editor with filesystem access. Extensions can execute code. Reads keys and configs. |
browser | Headless Chrome with JavaScript evaluation. 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, daemons | 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, and the URL is trivially rotated by deleting the program and re-creating it.
Hardening an Alias You Decide to Publish
Section titled “Hardening an Alias You Decide to Publish”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 also collide globally per server. - Restrict to an explicit base path.
target_path: "/api/v1"withallow_path_override: falseexposes only your public routes, even if other handlers exist in the same container. - 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.icuURLs are not. - Delete aliases before deleting containers. Orphaned aliases keep responding (with errors), and stale alias names are an attractive target if reassigned later.
The Rule of Thumb
Section titled “The Rule of Thumb”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 in the AI Era
Section titled “Security in the AI Era”Here is why this matters more than ever: 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. Not meaningfully. Not every line. Not every import. Not every network call.
This is not a failure of discipline. It is a consequence of scale. AI-generated code will have bugs, will have vulnerabilities, will make network calls you did not anticipate. This is not speculation — it is the current reality.
Hoody’s security model is designed for this reality:
-
Container isolation keeps a rogue AI-generated process inside its container. It cannot read other containers’ filesystems. It cannot signal other containers’ processes. Reaching the host takes a kernel or runtime vulnerability — the same caveat every isolation boundary carries.
-
Snapshot before AI makes changes. If the AI breaks something, restore in seconds. Not hours of debugging. Not
git bisect. Instant time travel. -
Network control means you decide where the AI’s 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 covers inbound service traffic. Every HTTP call into your container’s services passes the edge proxy — log it, inspect it, rate-limit it, or intercept it with hoody-exec hooks. Outbound calls your code makes are not proxied by the edge; configure a network exit to control their path, and add logging at that exit if you need outbound observability.
The Security Pyramid
Section titled “The Security Pyramid”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. Bare metal ownership eliminates entire classes of attacks even if a container is fully taken over — on a dedicated machine. On a free slice that layer is absent, and the pyramid is one layer shorter.
Practical Security Postures
Section titled “Practical Security Postures”Development: Open by Default
Section titled “Development: Open by Default”Permissions: None configuredURL security: Cryptographic (2^96)Firewall: Default allowNetwork: Direct NAT by default
Who can access: Only people who have the URLPerfect for development, experimentation, and internal tools. The URL is the password.
Staging: IP-Restricted
Section titled “Staging: IP-Restricted”Permissions: IP whitelist for office/VPNURL security: Cryptographic + IP checkFirewall: Allow from known IPsNetwork: Proxied exitAdds a second factor: even with the URL, you must be on the right network.
Production: Full Lockdown
Section titled “Production: Full Lockdown”Permissions: JWT for API, password for operators, IP for infraURL security: Cryptographic + auth requiredFirewall: Default deny, explicit allowNetwork: No dedicated public IPv4, controlled exitSnapshots: Hourly automatedBelt, suspenders, and a safety net. Every layer active.
# The CLI proxy state command can only ENABLE the proxy (--enable-proxy sends enable_proxy:true).# To DISABLE the proxy use the SDK or HTTP tab -- there is no --no-enable-proxy flag.
# Re-enable when investigation is completehoody containers proxy state --container $CONTAINER_ID --if-match file:v<N> --enable-proxyimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
// Disable proxy: kill-switch -- new requests then return 403, 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 stalelet { data } = await client.api.proxyPermissionsContainer.get(containerId);let ifMatch = data.etag ?? `file:v${data.file_version}`;await client.api.proxyPermissionsContainer.updateState(containerId, { enable_proxy: false }, { 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.proxyPermissionsContainer.get(containerId));ifMatch = data.etag ?? `file:v${data.file_version}`;await client.api.proxyPermissionsContainer.updateState(containerId, { enable_proxy: true }, { ifMatch });# Disable proxy (kill-switch -- new requests then return 403)curl -X PATCH "https://api.hoody.icu/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}'
# Container alive, but the proxy now answers 403 instead of forwarding# Re-enable when investigation is completecurl -X PATCH "https://api.hoody.icu/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}'Open by default, bulletproof when ready. Not because we are careless with defaults. Because the defaults are already cryptographically secure, and every additional layer is there when you need it.
Next: Snapshots — time travel as a security tool.