Skip to content
Hoody.com

Every cloud provider makes the same promise: your data is secure. The constraint is architectural rather than a matter of intent. To serve your data, a provider’s systems must be able to read it, which leaves the data reachable by that provider, by anyone who compromises it, and by any authority that compels it.

Hoody’s architecture leaves far less to take on trust. Your data lives on a machine rented to you and is served from that machine directly, with no central Hoody proxy tier in the path. That is a property of the design, not a claim that Hoody is more trustworthy.

The servers you rent are bare metal: physical machines, not virtual instances on shared hardware or containers on someone else’s hypervisor. You control the disk, the memory, and the network. Your containers run on that machine, and your data never transits a Hoody datacenter: the proxy that serves your URLs runs on your own server, not on Hoody’s.

None of this is a privacy feature layered on top; it is how the platform is built.


Your data does not live in a Hoody datacenter and does not pass through a Hoody proxy tier. It sits on a machine rented to you, served by software running on that same machine. Here is how that changes the trust picture:

LayerTraditional cloudHoody
HardwareShared with other tenantsDedicated bare metal you control
HypervisorProvider-controlledNone; containers run on your hardware
Disk encryptionProvider holds the keysLUKS on every host, always on; the key is never stored on the machine it unlocks, so seized hardware yields ciphertext
NetworkProvider can inspect trafficTLS terminates on your own rented server, with no central proxy tier in the path
BackupsProvider can read snapshotsSnapshots live on your disk
AI trainingYour data may be usedNot used for training; paid model calls reach the upstream provider

In traditional cloud, your data sits in the provider’s datacenter. In Hoody, it sits on a machine rented to you, and Hoody administers that machine rather than hosting your data. The server is a physical machine, the containers are processes on it, and the data is bytes on its disk. Hoody manages the orchestration layer (container creation, proxy routing, service coordination); the data itself stays on hardware you control.


hoody-files has no encrypting storage backend: crypt is one of the wrapping types that POST /api/v1/backends/{type} refuses, so there is no POST /api/v1/backends/crypt. To keep a file opaque even while the server runs, encrypt it where you run your code and upload the ciphertext. The key stays with you; hoody-files only ever stores and returns bytes it cannot read.

Terminal window
# Encrypt locally with a key from your environment, then upload the ciphertext.
# The contents come from the request body: pipe them in, or point --input at a local file.
openssl enc -aes-256-cbc -pbkdf2 -salt -pass env:ENCRYPTION_KEY \
-in ./api-keys.json -out ./api-keys.json.enc
hoody files upload secrets/api-keys.json.enc -c $CONTAINER_ID \
--input ./api-keys.json.enc
# Read the ciphertext back and decrypt it where the key lives
hoody files get secrets/api-keys.json.enc -c $CONTAINER_ID > ./api-keys.json.enc
openssl enc -d -aes-256-cbc -pbkdf2 -pass env:ENCRYPTION_KEY \
-in ./api-keys.json.enc -out ./api-keys.json

On disk, secrets/api-keys.json.enc is encrypted bytes, and so is what hoody-files serves: the service never holds your key, so a reader with only service or proxy access gets ciphertext. The decrypted copy exists only where you decrypt it. openssl enc does not authenticate the data, so for anything you must detect tampering in, use an authenticated format such as age or AES-GCM in your own code. Cloud providers that encrypt on their side, such as Proton Drive, are the other option; see Cloud storage.


For secrets that need to be available to applications over HTTP, use hoody-sqlite’s KV store as a secrets vault. The values are stored as written, unencrypted, in a SQLite file; encrypt them before writing if they must not rest in plaintext:

Terminal window
# Store secrets in the KV store. kv is a kit command, so it needs a target
# container: -c here, or HOODY_CONTAINER / HOODY_CONTAINER_ID, or ~/.hoody/config.json.
# --db is required; --create-db-if-missing on first use.
hoody kv set "vault:stripe_key" -c $CONTAINER_ID \
--db /hoody/databases/app.db --create-db-if-missing \
--body '"sk_live_abc123..."'
hoody kv set "vault:database_url" -c $CONTAINER_ID --db /hoody/databases/app.db \
--body '"postgres://user:pass@host:5432/db"'
hoody kv set "vault:jwt_secret" -c $CONTAINER_ID --db /hoody/databases/app.db \
--body '"your-256-bit-secret"'
# Retrieve secrets programmatically
hoody kv get "vault:stripe_key" -c $CONTAINER_ID --db /hoody/databases/app.db

The KV store is a SQLite database on your bare metal disk. Secrets rest on your rented machine and are served from it directly, with no central Hoody tier in the path. SQLite itself is not encrypted: the LUKS volume under it covers the machine being stolen. For a layer that still holds while the host is running, encrypt the values in your application before writing them, as in Encryption at rest.


Container isolation as the security boundary

Section titled “Container isolation as the security boundary”

Each container is a complete, isolated Linux environment, and the isolation is enforced at the operating system level:

┌──────────────────────────────────────────────────┐
│ YOUR BARE METAL SERVER │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ Container A │ │ Container B │ │ Cont. C │ │
│ │ │ │ │ │ │ │
│ │ Own files │ │ Own files │ │ Own files│ │
│ │ Own network │ │ Own network │ │ Own net │ │
│ │ Own procs │ │ Own procs │ │ Own procs│ │
│ │ Own users │ │ Own users │ │ Own users│ │
│ │ │ │ │ │ │ │
│ │ CANNOT SEE │ │ CANNOT SEE │ │ CANNOT │ │
│ │ B or C │ │ A or C │ │ SEE A, B │ │
│ └─────────────┘ └─────────────┘ └──────────┘ │
│ │
│ Shared: CPU, RAM, Disk (but isolated views) │
└──────────────────────────────────────────────────┘

What that isolation means in practice:

  • Container A cannot read Container B’s files, even though they share the same physical disk
  • A process in Container A cannot see or kill processes in Container B
  • Network traffic is isolated: containers cannot sniff each other’s traffic
  • A compromised container cannot read another container’s filesystem or signal its processes. Writable shared mounts, credentials and services it can reach are the paths that do cross that line. Containers also share the host kernel and its capacity, so isolation alone does not bound resource exhaustion
  • An AI agent running in Container A runs as the container’s user account, with exactly the network reach you leave it: egress starts open, and the firewall pattern below closes it

When you give an AI agent access to a container, it can reach everything that container’s user account can: your other containers’ files and processes are out of reach, while writable shared mounts, credentials it holds and services its network reach allows are not. Containers also share the host kernel and its capacity, so an unbounded workload can still affect the machine unless you set limits. No isolation boundary is absolute; the point is that the unit of compromise is small, disposable, and restorable from a snapshot in seconds.


Realm-restricted tokens for tenant isolation

Section titled “Realm-restricted tokens for tenant isolation”

When building multi-tenant applications, use realms to create isolated API scopes:

Terminal window
# NOTE: Realms are not created. A realm is any 24-hex identifier you attach to
# resources via realm_ids; it starts existing the moment a resource carries it.
# `hoody realms list` reports the realm IDs already in use across your resources:
hoody realms list
# Create containers within each realm by assigning the label at creation time
hoody containers create --project $PROJECT_ID \
--server-id $SERVER_ID \
--name "acme-app" \
--realm-ids 507f1f77bcf86cd799439011 \
--hoody-kit
hoody containers create --project $PROJECT_ID \
--server-id $SERVER_ID \
--name "globex-app" \
--realm-ids 507f1f77bcf86cd799439012 \
--hoody-kit

Realm-scoped API tokens can only access containers within their realm. A token restricted to the acme realm cannot see, list, or access any container in the globex realm. The isolation is enforced at the API level.

Layer encryption for sensitive data:

// Application-level encryption for specific fields, on top of the disk encryption
// @mode serverless
const SQLITE = "https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com";
// Store with application-level encryption
const crypto = require('crypto');
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const sensitiveData = JSON.stringify({ ssn: '123-45-6789', salary: 150000 });
let encrypted = cipher.update(sensitiveData, 'utf8', 'hex');
encrypted += cipher.final('hex');
await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
transaction: [
{ query: "CREATE TABLE IF NOT EXISTS employee_records (name TEXT, encrypted_data TEXT, iv TEXT)" },
{ query: "INSERT INTO employee_records (name, encrypted_data, iv) VALUES (?, ?, ?)", values: ["Alice Chen", encrypted, iv.toString('hex')] }
]
})
});

The application encrypts sensitive fields before hoody-sqlite stores them, so the column holds ciphertext even for someone who can read the database while the host is running. hoody-sqlite does not encrypt the database file itself.

Prevent containers from sending data to unauthorized destinations:

Terminal window
# Start from a clean firewall (reset detaches the ACL and returns the
# container to an open network state), then deny outbound IPv4. The two
# allow rules below are documentation of intent: the drops that follow are
# evaluated first, so no new outbound IPv4 that they match is left. Host DNS
# and established connections still pass. Container networking is IPv4-only.
# `reset` is destructive, so it asks for confirmation; pass -y to run unattended.
hoody firewall reset -c $CONTAINER_ID -y
# Resolve the hostnames you want to allow to IPv4/CIDR first; the firewall
# API only accepts numeric destinations (host `dig +short api.stripe.com`).
STRIPE_CIDR=$(dig +short api.stripe.com | awk '{print $1"/32"; exit}')
GITHUB_CIDR=$(dig +short api.github.com | awk '{print $1"/32"; exit}')
hoody firewall egress create -c $CONTAINER_ID \
--action allow --protocol tcp --destination-port 443 \
--destination "$STRIPE_CIDR" --description "Allow Stripe API"
hoody firewall egress create -c $CONTAINER_ID \
--action allow --protocol tcp --destination-port 443 \
--destination "$GITHUB_CIDR" --description "Allow GitHub API"
# Deny outbound IPv4. Add one rule per protocol to cover tcp, udp and icmp4.
# These drops are evaluated before the allow rules above, so they also cover
# Stripe and GitHub: drop the complement of a port instead if you need one open.
# tcp and udp rules must carry a destination port; 1-65535 covers every one of them.
# The firewall API takes IPv4 destinations only, which is the whole of container
# networking: containers are created with IPv6 disabled and their network has no
# IPv6 address.
hoody firewall egress create -c $CONTAINER_ID \
--action drop --protocol tcp --destination "0.0.0.0/0" --destination-port 1-65535 \
--description "Deny all TCP egress"
hoody firewall egress create -c $CONTAINER_ID \
--action drop --protocol udp --destination "0.0.0.0/0" --destination-port 1-65535 \
--description "Deny all UDP egress"
hoody firewall egress create -c $CONTAINER_ID \
--action drop --protocol icmp4 --destination "0.0.0.0/0" --description "Deny all ICMP egress"

Because the drops are evaluated before the allow, the container cannot open new IPv4 connections that they match, the Stripe rule included. DNS to the container’s host is accepted ahead of them, established connections keep flowing, and an approved destination is itself a way out. The firewall API accepts IPv4 addresses and CIDR ranges only, which covers the whole of container networking: containers are created with IPv6 disabled and their network carries no IPv6 address. That combination is what you want when running untrusted code or AI agents that might attempt to send data externally.


Physical servers have physical locations, which keeps compliance questions concrete:

Data residency: A server in Frankfurt holds your data on a disk in Frankfurt. It is not “primarily” there, and it is not “replicated from” there.

GDPR: European personal data lives on European hardware, and container data stays on that machine unless you send it elsewhere. There is no “our servers might be anywhere” ambiguity.

HIPAA: Protected health information sits on dedicated hardware with encrypted filesystems, firewall-controlled network access, and container isolation between patient datasets.

SOC 2: The audit trail comes from HTTP request logs, access control from proxy permissions, encryption from full-disk encryption plus the keys you hold yourself, and isolation from containers. Every compliance requirement maps to an HTTP-observable, configurable control.

┌────────────────────────────────────────────────┐
│ YOUR SERVER: Frankfurt, Germany │
│ Physical address: DataCenter GmbH, Room 4B │
│ │
│ Container: patient-records │
│ ├── Disk encryption (LUKS), your own file keys│
│ ├── Firewall: egress deny-all │
│ ├── Proxy: IP whitelist (clinic IPs only) │
│ ├── Realm: healthcare-prod │
│ └── Snapshots: daily, 90-day retention │
│ │
│ Data location: This building. This rack. │
│ Data access: Clinic IPs only. │
│ Data encryption: LUKS disk, plus your own keys.│
│ Data retention: 90-day snapshot history. │
│ Audit trail: Every HTTP request logged. │
└────────────────────────────────────────────────┘

A shared cloud VM cannot state that posture: the hardware is shared and the hypervisor belongs to the provider.


An AI agent with access to a container (terminal, files, database, browser) has the same access as a developer: it can read credentials, query databases, and browse the filesystem. That reach is what makes isolation worth setting up before the agent runs.

On shared infrastructure, a compromised or misbehaving AI agent could:

  • Exfiltrate data through network requests
  • Access other tenants’ resources through hypervisor vulnerabilities
  • Persist malicious code that survives container restarts
  • Send your data to the AI provider’s training pipeline

On Hoody’s bare metal + container architecture:

  • Container isolation keeps the agent inside its own container
  • Firewall rules prevent unauthorized network communication
  • Snapshots put the container’s own disk back, restarting it if it was running; they do not undo a remote call, a write to mounted storage, or a credential the agent disclosed
  • Bare metal means no hypervisor attack surface
  • Full-disk encryption is always on, on every host, so a drive read outside the running machine yields ciphertext
  • Encrypting files yourself adds the layer that survives a live host: files you encrypt before uploading stay ciphertext even to something reading the unlocked volume
Terminal window
# The AI safety workflow:
# 1. Create an isolated container for the experiment and capture its ID
EXPERIMENT_ID=$(hoody containers create --project $PROJECT_ID \
--server-id $SERVER_ID \
--name "ai-experiment" \
--hoody-kit -o json | jq -r '.id')
# 2. Lock down the network: reset to a clean baseline, then drop outbound
# IPv4. Host DNS and established connections are accepted ahead of these
# rules, so this is not a fully offline container.
# `reset` is destructive, so pass -y to skip its confirmation prompt.
# tcp and udp rules must carry a destination port; 1-65535 covers every one.
hoody firewall reset -c $EXPERIMENT_ID -y
hoody firewall egress create -c $EXPERIMENT_ID \
--action drop --protocol tcp --destination "0.0.0.0/0" --destination-port 1-65535 \
--description "Deny all TCP"
hoody firewall egress create -c $EXPERIMENT_ID \
--action drop --protocol udp --destination "0.0.0.0/0" --destination-port 1-65535 \
--description "Deny all UDP"
hoody firewall egress create -c $EXPERIMENT_ID \
--action drop --protocol icmp4 --destination "0.0.0.0/0" --description "Deny all ICMP"
# The agent still needs its model provider: these rules also block new
# connections to it, so add an accept rule for your model endpoint (or route
# inference through a proxy you control) before step 4, or the turn fails.
# 3. Snapshot clean state
hoody snapshots create --container $EXPERIMENT_ID \
--alias "clean-slate"
# 4. Let the AI agent run. hoody-agent is an in-container Kit service reached
# through the Hoody Proxy, not the management Hoody API. Open a session bound to the container,
# then dispatch a turn into it (auto-approve answers confirm gates so the
# turn runs unattended). For a session-less one-shot, use
# `hoody agent headless start` instead.
# --x-hoody-realm sets the agent's realm scope ("global" or a 24-hex id); it is the
# same selector the HTTP tab spells as ?realm=, and the header wins when both are sent.
SESSION_ID=$(hoody agent sessions create --x-hoody-realm global \
-c $EXPERIMENT_ID -o json | jq -r '.session_id')
hoody agent sessions turns run --id "$SESSION_ID" -c "$EXPERIMENT_ID" --x-hoody-realm global \
--policy auto_approve \
--text "Analyze this dataset and build a classification model"
# 5. Inspect results
# 6. Restore clean state if needed. The snapshot's name IS the alias
# (stripped to letters, digits, _ and -), so restore by "clean-slate".
# Restore restarts the container if it was running, and answers 409
# while its network policy is still pending.
hoody snapshots restore -c $EXPERIMENT_ID --name "clean-slate" -y

The AI agent has full autonomy inside a network-restricted container. It can read data, write code, and run experiments, but the deny-all rules block the outbound IPv4 paths they match. DNS to the container’s host is accepted before those rules and established connections keep flowing, so treat this as a strong restriction rather than proof that nothing can leave. The firewall is IPv4-only, and so is container networking: containers are created with IPv6 disabled and their network carries no IPv6 address. When the experiment is done, inspect the results; if anything looks wrong, restore the clean snapshot. Within those limits, the work stays on your hardware.


The security posture layers eight controls:

Layer 1: Bare Metal → No shared hardware, no hypervisor attack surface
Layer 2: Container → Process isolation, filesystem isolation
Layer 3: Firewall → Network egress control, default-allow; add rules to restrict
Layer 4: Proxy Permissions → Authentication before HTTP reaches the container
Layer 5: Encrypted FS → LUKS on every host by default; encrypting files yourself adds a live-host layer
Layer 6: Application → Field-level encryption, input validation
Layer 7: Snapshots → Rollback capability, audit via diff
Layer 8: Realms → API-level tenant isolation

Each layer is independently configurable through HTTP, and each can be audited and changed the same way. The layers are not fully independent of one another (containers on a host share a kernel), but they fail separately: compromising one does not hand an attacker the rest.