# Security & Permissions

**Page:** concepts/security

[Download Raw Markdown](./concepts/security.md)

---

# 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](/concepts/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

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^96
```

**The 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.


Wildcard TLS certificates (`*.containers.hoody.icu`) mean your specific container URLs never appear in Certificate Transparency logs. No one can discover your container addresses by scanning public certificate databases.


---

## Layer 2: Container Isolation

Every container is a sealed boundary. Not a suggestion. Not a convention. A kernel-enforced perimeter.

### 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

Each container has its own network namespace, its own IP address, its own routing table. Containers do not share a network bridge. They communicate through the proxy via HTTP, the same way any two machines on the internet communicate. No internal network to eavesdrop on.

### 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

This is not application-level sandboxing. This is kernel-level enforcement:

| Technology | What It Does |
| :--- | :--- |
| **Linux namespaces** | Isolate PIDs, network, mounts, users, IPC |
| **seccomp filters** | Restrict available syscalls -- containers cannot call dangerous kernel functions |
| **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. It does not escalate. It does not escape. Delete it, snapshot a clean one, and move on.

---

## 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.

**Hoody containers run on YOUR bare metal servers.** You rent or own the physical machine. No other customer has containers on your server. No shared hypervisor. No neighbor you cannot audit. No "noisy neighbor" performance problems.

The security implications:

- **Side-channel attacks eliminated.** No shared CPU cache to exploit. No shared memory bus to sniff.
- **No hypervisor escape risk.** There is no hypervisor to escape from. Your containers run on bare Linux.
- **Physical control.** Your server, your disk encryption keys, your network configuration.
- **Performance predictability.** Every CPU cycle, every memory byte, every disk IOPS is yours. No random slowdowns from strangers' workloads.


  
    ```bash
    # List your servers -- these are YOUR bare metal machines
    hoody servers list

    # Each server runs its own proxy, its own containers
    # No shared infrastructure with other customers
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Your servers are your infrastructure
    const servers = await client.api.serverRental.list();

    // Each server: your hardware, your containers, your proxy
    // Disk encryption (LUKS AES-256) on your metal
    // No shared hypervisor, no shared kernel with strangers
    ```
  
  
    ```bash
    # Your servers
    curl "https://api.hoody.icu/api/v1/servers" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Each server response includes:
    # - server_name (your proxy address)
    # - container count (all yours)
    # - disk encryption status
    ```
  


---

## 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

| 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

**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 everything
```

### Service-Level Granularity

Permissions are not all-or-nothing. Each authentication group gets fine-grained access per service:


  
    ```bash
    # Set container-level proxy permissions
    hoody containers proxy permissions replace -c $CONTAINER_ID \
      --project $PROJECT_ID \
      --groups internal='{"type":"ip","range":"10.0.0.0/8"}' \
      --permissions internal='{"terminal":true,"files":true,"display":false,"sqlite":false}' \
      --default deny
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Granular permissions per service
    await client.api.proxyPermissionsContainer.replace(containerId, {
      project: PROJECT_ID,
      container: containerId,
      groups: {
        humans: { type: 'password', password: 'secure-pass' },
        agents: { type: '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'
    });
    ```
  
  
    ```bash
    # Production lockdown: only HTTP traffic from known IPs
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "groups": {
          "load_balancer": {
            "type": "ip",
            "range": "10.0.0.0/8"
          },
          "operators": {
            "type": "jwt",
            "secret": "your-jwt-secret",
            "algorithm": "HS256"
          }
        },
        "permissions": {
          "load_balancer": {
            "http": true
          },
          "operators": {
            "terminal": true,
            "files": true,
            "display": true,
            "sqlite": true,
            "exec": true
          }
        },
        "default": "deny"
      }'
    ```
  


---

## 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-deny: only explicitly allowed traffic passes |

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

By default, containers have **no IPv4 address**. All outbound traffic routes through the Hoody Proxy or configured network exits. This prevents containers from making arbitrary connections to the internet.

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
- **WireGuard VPN** integration
- **Commercial VPN providers** (Mullvad, iVPN, AirVPN) with zero in-container config
- **Block mode** to prevent ALL outgoing traffic
- **Custom DNS servers** (up to 4)

A compromised container that tries to phone home is stopped at the network boundary. It cannot add exit routes. It cannot change DNS. It cannot bypass the configured egress path.

---

## Layer 7: Disk Encryption

Every server supports LUKS disk encryption (AES-256) at rest. Encrypted swap. Encrypted temporary files. The physical disk is unreadable without the encryption key, even if the hardware is physically stolen.

---

## Layer 8: Realms

[Realms](/foundation/hoody-api/realms/) provide API-level isolation. Different realms are different universes:

```
https://realm-a.api.hoody.icu  →  sees only Realm A's containers
https://realm-b.api.hoody.icu  →  sees only Realm B's containers
```

Auth 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

[Proxy aliases](/foundation/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:**

1. **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…`.
2. **Surface exposure.** The alias still routes to a specific [program](/kit/). 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

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](/kit/exec/)) | Scripts you wrote with explicit handlers and routes. Behaves like any HTTP framework. |
| **`pipe`** ([hoody-pipe](/kit/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](/kit/tunnel/)) | HTTP-only forwarding of a local service through the proxy. Auth runs at the proxy boundary. |

These four are the **public diffusion set**. They expose HTTP semantics — nothing more — and rely on you to decide what the application returns. Combine them with [proxy permissions](/foundation/proxy/permissions/) and you have a clean, professional URL backed by real authentication.

### 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.


**An alias does not replace permissions.** If you must alias a privileged program (e.g. an internal operations terminal at `ops-shell.node-us.containers.hoody.icu`), treat the alias name as public information and configure [proxy permissions](/foundation/proxy/permissions/) accordingly: IP whitelist, JWT or password, default-deny. Set `expires_at` so forgotten aliases do not outlive their need.


### 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-api` is not enumerable; `api`, `app`, `prod` are. Generic names also collide globally per server.
- **Restrict to an explicit base path.** `target_path: "/api/v1"` with `allow_path_override: false` exposes 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.com` to 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 `*.containers.hoody.icu` URLs 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

> 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

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 means a rogue AI-generated process cannot escape.** It runs in a container. It cannot read other containers' filesystems. It cannot signal other containers' processes. It cannot access the host.

- **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 the AI's code cannot phone home.** No IPv4 by default. Configured exit paths. Host-level firewall rules. A container running AI-generated code that tries to exfiltrate data hits a wall it cannot modify.

- **HTTP observability means you can watch everything.** Every HTTP call the AI's code makes flows through the proxy. Log it. Inspect it. Rate-limit it. Intercept it with [hoody-exec](/kit/exec/) for real-time analysis.


Security is never absolute. Container isolation mitigates lateral movement but does not prevent application-level vulnerabilities within a container. Supply chain attacks (compromised npm packages, malicious Docker images) remain an industry-wide challenge. Hoody provides containment and rapid recovery, not prevention of all possible attacks.


---

## 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 IPv4 default, controlled exit paths
├─────────────────────────┤
│   Container Isolation   │  Namespaces, seccomp, hardened kernel
├─────────────────────────┤
│   Bare Metal Ownership  │  Your hardware, no shared hypervisor
├─────────────────────────┤
│   Disk Encryption       │  LUKS AES-256 at rest
├─────────────────────────┤
│   Realm Isolation       │  API-level multi-tenancy
├─────────────────────────┤
│   URL Unguessability    │  2^96 keyspace, no enumeration
└─────────────────────────┘
```

Each layer is independent. A failure in one layer does not compromise the others. 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.

---

## Practical Security Postures

### Development: Open by Default

```
Permissions: None configured
URL security: Cryptographic (2^96)
Firewall: Default allow
Network: Proxied exit

Who can access: Only people who have the URL
```

Perfect for development, experimentation, and internal tools. The URL is the password.

### Staging: IP-Restricted

```
Permissions: IP whitelist for office/VPN
URL security: Cryptographic + IP check
Firewall: Allow from known IPs
Network: Proxied exit
```

Adds a second factor: even with the URL, you must be on the right network.

### Production: Full Lockdown

```
Permissions: JWT for API, password for operators, IP for infra
URL security: Cryptographic + auth required
Firewall: Default deny, explicit allow
Network: No IPv4, controlled exit
Snapshots: Hourly automated
```

Belt, suspenders, and a safety net. Every layer active.


  
    ```bash
    # Disable proxy for maintenance (503 all requests)
    hoody containers proxy state -c $CONTAINER_ID

    # Re-enable when ready
    hoody containers proxy state -c $CONTAINER_ID --enable-proxy
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Emergency lockdown: disable all proxy access
    await client.api.proxyPermissionsContainer.updateState(containerId, {
      enable_proxy: false
    });
    // All URLs now return 503 -- container keeps running internally

    // Re-enable when investigation is complete
    await client.api.proxyPermissionsContainer.updateState(containerId, {
      enable_proxy: true
    });
    ```
  
  
    ```bash
    # Emergency lockdown
    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" \
      -d '{"enable_proxy": false}'

    # Container is alive but unreachable via proxy
    # Re-enable when investigation is complete
    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" \
      -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](/concepts/snapshots/) -- time travel as a security tool.