Skip to content
Hoody.com

The Hoody Proxy enforces authentication and authorization for ALL container services. Configure who can access what, at what level, with complete granularity.

After understanding the proxy architecture and creating aliases, you need to understand how to control access to your container services.


Official Technical Reference:

This Foundation page explains permission concepts and configuration strategies. For complete endpoint documentation:

Project-Level Permissions:

Container-Level Permissions:

Container Proxy Hooks (MITM traffic interception):

Container Proxy Settings (root enable/default policy):


Hoody’s permission system is multi-layered:

┌─────────────────────────────────────┐
│ Hoody API Authentication │ ← User login, API tokens
│ (api.hoody.icu) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Hoody Proxy Permissions │ ← THIS PAGE
│ (Container access control) │
│ │
│ ├─ Project-Level Permissions │ ← Apply to all containers
│ └─ Container-Level Permissions │ ← Override for specific containers
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Container Services │
│ (terminal, display, files, etc.) │
└─────────────────────────────────────┘

Two separate systems:

  1. Hoody API Auth - Access to platform management (create containers, configure firewall)
  2. Proxy Permissions - Access to container services (execute commands, read files, view displays)

This page covers Proxy Permissions - how to control who can use your container’s terminal, files, displays, and other services.


Groups define authentication methods.

Each group specifies HOW users prove their identity:

  • JWT - Token-based with secret verification and claims validation
  • Password - Username/password via HTTP Basic Auth
  • IP - Allow/deny based on client IP address or CIDR range
  • Token - Bearer token validation
  • Hoody Identity - Native Hoody identity-claim verification (hoody-identity)

Example:

{
"groups": {
"developers": {
"type": "ip",
"range": "203.0.113.0/24"
},
"customers": {
"type": "jwt",
"secret": "a-long-random-signing-key-with-32-plus-chars",
"algorithm": "HS256",
"sources": ["header:Authorization"]
},
"admin": {
"type": "password",
"username": "admin",
"password": "hashed-password",
"algorithm": "sha256",
"salt": "unique-salt"
}
}
}

Permissions map groups to programs with flexible instance control.

Each group gets specific access to container programs using:

  • true - Allow ALL instances
  • false - Deny ALL instances
  • number - Allow ONLY this instance/port (e.g., 1 allows instance 1)
  • array - Allow SPECIFIC instances/ports (e.g., [1, 2])
  • "8000-8100" - Allow a port range (network programs only)
  • "*" - Allow every instance (any indexed program; rejected for http/https, which must name ports)

Indexed programs (terminal, display, code, browser, …) accept all of the above. The programs files, exec, services, and notifications accept only true/false.

http and https are not programs — they are the transport labels in a container hostname (<project>-<container>-http-8080). Their permission cell gates raw ports, so it must say which ports it opens:

"http": [8080] // this port only
"http": [80, 8080] // these ports
"http": 8080 // shorthand for one port
"http": "8000-8100" // an inclusive range

A bare "http": true or "http": "*" is rejected — the API returns 422 Validation Error naming the port form, and a range covering more than 16384 ports is rejected the same way. There is no “all ports” grant: name the ports your application serves and you get exactly those.

"http": false cannot be written either — every bare boolean is rejected. To deny raw-port access, omit the http cell entirely (with "default": "deny" nothing is granted). Legacy documents that already contain "http": false still act as an explicit deny at the proxy.

A port grant opens whatever is listening on that port inside your container.

Ports belonging to Hoody’s own always-on services are refused outright, so a grant can never reach them — the container daemon, file server, terminal, code server, browser, exec, the agent API and the shared display server are all off limits, and an attempt to name one is denied even inside a wide range.

What a grant does not do is inspect what else your container put on the port. If you started a display instance that happens to occupy the port you granted, the grant reaches that display, and a "display": false cell in the same group will not prevent it — the request arrives as a raw port, not as the display program. This matters only if you both deny a program and grant a port that program happens to be using; grant the ports your application serves and it does not arise.

ssh is not a permission gate. An ssh hostname carries no port and routes to the dedicated SSH proxy, so an ssh cell in this document has no effect; use the container’s SSH key configuration instead.

{
"permissions": {
"developers": {
"terminal": [1, 2], // Allow terminal instances 1 and 2 only
"files": true, // Allow all file service instances
"display": 1, // Allow only display instance 1
"http": [8080] // Allow port 8080 only
},
"customers": {
"http": [8080], // Allow ONLY port 8080
"terminal": false, // Deny all terminal access
"files": false // Deny all file access
},
"admin": {
"terminal": true, // Full access to all terminals
"files": true, // Full access to files
"display": true, // Full access to all displays
"http": [8080, 8443] // Allow ports 8080 and 8443
}
}
}

What happens when no group matches?

{
"default": "deny" // or "allow"
}
  • “deny” - Block access if no group matches (secure by default)
  • “allow” - Permit access if no group matches (open by default)

Permissions can be set at two levels:

Project Level
├─ Applies to ALL containers in project
└─ Good for consistent team access
Container Level
├─ Overrides project settings for specific container
└─ Good for exceptions (public container in private project)

If both configured: The two documents merge, and container-level values take precedence wherever they overlap.


By default, containers have NO proxy permissions configured.

This means:

  • Anyone with the URL can access all services
  • No authentication required
  • No group matching
  • Pure “security by unguessable URL”

The URL contains:

https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu
└────────24-char hex────┘ └────────24-char hex────┘

2^96 × 2^96 = 2^192 possible combinations for project+container pair.

Practically unguessable. Share URL = grant access — but anyone the URL reaches (pasted into Slack, captured in a screenshot, logged by a referer header, cached by a browser extension) can hit the container directly. Unguessability prevents blind discovery; it does not contain leaks or replace access control.

This enables:

  • Instant collaboration (just share URL)
  • Zero configuration (works immediately)
  • Multiplayer by default (anyone with URL can join)

When you need real access control: Add proxy permissions — don’t rely on URL secrecy for anything sensitive, and never paste container URLs into public channels, public dashboards, or untrusted third-party tools.


Apply authentication to ALL containers in a project:

Terminal window
# Set project-level proxy permissions (IP-restricted team access).
# Whole-document `permissions replace` is currently unavailable in the CLI —
# build the document one field at a time. Each write bumps file_version, so
# re-read the If-Match ETag before every command with
# `hoody projects proxy permissions get --project $PROJECT_ID -o json`.
hoody projects proxy groups ip set --project $PROJECT_ID \
--group-name team --range 203.0.113.0/24 --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name team --program terminal --access '[1,2]' --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name team --program files --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name team --program display --access 1 --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name team --program http --access '[8080]' --if-match file:v<N>
hoody projects proxy default --project $PROJECT_ID --default deny --if-match file:v<N>
PATCH Set project-level proxy permissions
/api/v1/projects/{project_id}/proxy/permissions
Click "Run" to execute the request

Now ALL containers in this project:

  • Require IP from 203.0.113.0/24 range
  • Grant access to terminal, files, display, http
  • Deny all other access

Override project settings for specific container:

Terminal window
# Override permissions for a specific container (public HTTP only).
# Whole-document `permissions replace` is currently unavailable in the CLI —
# build the document one field at a time, re-reading the If-Match ETag before
# every command with `hoody containers proxy permissions get -c $CONTAINER_ID -o json`.
hoody containers proxy groups ip set -c $CONTAINER_ID \
--group-name public --range 0.0.0.0/0 --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name public --program http --access '[8080]' --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name public --program terminal --access false --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name public --program files --access false --if-match file:v<N>
hoody containers proxy default -c $CONTAINER_ID --default deny --if-match file:v<N>
PATCH Set container-level proxy permissions (overrides project)
/api/v1/containers/{container_id}/proxy/permissions
Click "Run" to execute the request

This container now:

  • Accepts requests from any IP (public access)
  • Only HTTP services allowed
  • Terminal and files denied for the public group
  • Overrides the project document only where entries overlap — project groups this document does not name stay in force

Use case: Public demo container in an otherwise private project.


Groups define HOW users authenticate.

Validate JSON Web Tokens:

{
"groups": {
"authenticated_users": {
"type": "jwt",
"secret": "a-long-random-signing-key-with-32-plus-chars",
"algorithm": "HS256",
"sources": ["header:Authorization", "cookie:auth_token"],
"claims": {
"iss": "mycompany.com",
"aud": "production-api"
}
}
}
}

Parameters:

  • secret - JWT signing key (required). For HS256 the key must be at least 32 characters and must not contain a common weak word (secret, password, 123456, test, dev, default) — the API returns 422 otherwise. For RS256/ES256 this is the PEM-encoded public key.
  • algorithm - HS256, RS256, or ES256 (required)
  • sources - Where to look for the token (required). Each entry must match header:Name or cookie:Name — JWTs can only be read from a header or a cookie.
  • claims - Required JWT claims to validate (optional)

Token sources examples:

  • header:Authorization - Bearer token in Authorization header
  • cookie:session - JWT in session cookie

Use case: Your application issues JWTs to users, Hoody validates them at the proxy level.

Gate a service on “a valid Hoody user” — natively. The proxy verifies the ED25519 identity claim Hoody mints (via POST /api/v1/users/auth/identity-claim), rejects everything else, and injects verified identity headers your app can trust:

{
"groups": {
"hoody_users": {
"type": "hoody-identity",
"audience": "myapp.example.com",
"allow_types": ["user", "admin"],
"users": ["507f1f77bcf86cd799439011"],
"max_age_seconds": 86400,
"expose_type": false
}
}
}

Parameters:

  • audiencerequired. The claim’s aud must equal this exactly. Clients mint a bound claim with POST /api/v1/users/auth/identity-claim { "audience": "myapp.example.com" }. Unbound (login-issued) claims are always rejected — an audience gate can never be satisfied by a generic 30-day login claim.
  • sources — optional; header sources within the reserved namespace (default ["header:X-Hoody-Identity-Claim"]). Clients send the claim as <kid>.<payload_b64>.<signature_hex>.
  • allow_types — optional subset of ["user", "admin"] (default both).
  • users — optional allowlist of user IDs (up to 256) — “my users only” enforced at the proxy.
  • max_age_seconds — optional freshness gate on the claim’s iat (min 300).
  • expose_type — also inject X-Hoody-Identity-Type (user|admin). Off by default so platform-admin isn’t mistaken for app-admin.

What your app receives: verified X-Hoody-Identity-Sub (user ID) and X-Hoody-Identity-Username headers, injected by the edge AFTER it unconditionally strips any client-supplied X-Hoody-Identity-* — the values are trustworthy on every proxied request, and the claim credential itself never reaches your app. Key material is operator-owned (the proxy’s trust keyring) — group config never carries keys.

Building a real gate — a group alone is NOT a gate. The file must also set explicit service cells and "default": "deny", or unauthenticated traffic falls through to the default policy:

{
"groups": { "hoody_users": { "type": "hoody-identity", "audience": "myapp.example.com" } },
"permissions": { "hoody_users": { "run": true } },
"default": "deny"
}

After activating, verify anonymous is denied (curl without the header must get 403). Once a gate file has been active, a missing/undistributed file fails closed (deny), never open.

Scope (v1): programmatic clients — CLIs, SDKs, native apps, server-to-server (browsers can’t attach custom headers on navigation; use a proxy hook for browser apps). Applies on the reverse-proxy path only (not the forward-proxy/CONNECT products) and gates WebSocket handshakes (established sockets outlive claim expiry — terminate server-side if you need live cutoff).

HTTP Basic Auth with username/password:

{
"groups": {
"admin_access": {
"type": "password",
"username": "admin",
"password": "hashed-password-here",
"algorithm": "sha256",
"salt": "unique-salt-value"
}
}
}

Parameters:

  • username - Exact username match required
  • password - Plain or hashed password
  • algorithm - Hashing algorithm (sha256)
  • salt - Salt for password hashing

Browser behavior: Browser will show HTTP Basic Auth prompt automatically.

Why Password Auth is Underrated:

While it’s an old protocol, password authentication is remarkably portable:

  • Works everywhere - No JWT libraries needed, no token management
  • Browser native - Built-in auth prompts on all browsers
  • Zero dependencies - Just username + password, works on any HTTP client
  • Perfect for humans - Simple, intuitive, memorable credentials
  • URL embeddable - https://user:pass@domain.com works in most contexts
  • Emergency access - When OAuth is down, password auth still works
  • Low complexity - No token expiration, no refresh flows, no claims validation

Use cases:

  • Simple admin access
  • Quick demos and prototypes
  • Temporary contractor access
  • Emergency backdoors
  • Internal tools where JWT overhead isn’t worth it
  • Any scenario where portability > sophistication

Allow/deny by client IP address:

{
"groups": {
"office_network": {
"type": "ip",
"range": "203.0.113.0/24"
},
"vpn_users": {
"type": "ip",
"range": "198.51.100.0/24"
}
}
}

Parameters:

  • range - IPv4 CIDR notation (e.g., 203.0.113.50/32 for single IP)

Use case: Restrict to office network, VPN, known IPs.

Note: The proxy sees real client IPs (not proxy IPs) thanks to Hoody’s netfilter hooks, so IP-based auth works perfectly.

Validate bearer tokens:

{
"groups": {
"api_partners": {
"type": "token",
"value": "token-abc-123",
"header": "X-Api-Token"
}
}
}

A token group carries a single value plus exactly one of header, cookie, or param specifying where the token is read from.

Use case: Distribute tokens to API consumers, partners, integrations.

Authentication Without Headers (hoody-curl Workaround)

Section titled “Authentication Without Headers (hoody-curl Workaround)”

Problem: Some environments can’t send custom headers:

  • Browser bookmarks (just URLs)
  • QR codes (GET-only)
  • Email links (no header control)
  • Restricted platforms (iOS Shortcuts, some automation tools)

Solution: Use hoody-curl to transform authenticated requests into simple GET URLs.

How it works:

Terminal window
# Requires ability to send Authorization header
curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu/execute" \
-H "Authorization: Bearer jwt-token-here" \
-H "Content-Type: application/json" \
-d '{"command": "ls -la"}'
# Can't do this from browser bookmark
# Can't do this from QR code
# Can't embed in simple URL

Practical example - Bookmark to execute deployment:

// Create a bookmark URL that deploys your app
const curlService = "https://67e89abc...890abc-curl-1.node-us.containers.hoody.icu";
const targetService = "https://67e89abc...890abc-exec-1.node-us.containers.hoody.icu/api/deploy";
const authToken = "your-jwt-token";
const bookmarkUrl = `${curlService}/api/v1/curl/request?` + new URLSearchParams({
url: targetService,
method: 'POST',
header: `Authorization: Bearer ${authToken}`,
json: JSON.stringify({ environment: 'production' })
});
// Save as bookmark: "Deploy Production"
// Click bookmark → Deployment triggered
// No terminal needed, no curl command, just a click

Use cases:

  • Emergency deployments - Bookmark for instant deploy
  • Mobile access - QR code to trigger workflows
  • Email notifications - “Click here to approve” links
  • Restricted automation - iOS Shortcuts, Zapier webhooks
  • Simple sharing - Send URL instead of curl command

See: Hoody cURL → for complete documentation on wrapping HTTP requests.


Each program supports flexible instance control:

Access-rule value types (boolean, number, array, port-range string, wildcard):

{
"permissions": {
"developers": {
"terminal": true, // Allow ALL terminal instances
"files": false // Deny ALL file service instances
}
}
}

Use when: Simple all-or-nothing access

Programs you can configure:

  • http - HTTP services (port-based: http-80, http-3000, etc.)
  • ssh - SSH access to container
  • terminal - Hoody Terminal service instances
  • display - Hoody Display (desktop / web-UI) instances
  • files - Hoody Files service
  • exec - Hoody Exec script execution
  • services - Container service endpoints
  • notifications - Notification service

Real scenario: Container with 5 terminal instances for different teams:

{
"permissions": {
"frontend_team": {
"terminal": [1, 2], // Frontend devs get terminals 1 and 2
"display": 1,
"http": [8080]
},
"backend_team": {
"terminal": [3, 4], // Backend devs get terminals 3 and 4
"display": 2,
"http": [8080]
},
"ops_team": {
"terminal": true, // Ops gets ALL terminals
"display": true, // ALL displays
"http": [8080]
}
}
}

Each team isolated to specific instances while sharing the same container.


Internal team, broad access, IP-restricted:

Terminal window
# Configure IP-restricted developer access for entire project.
# Whole-document `permissions replace` is currently unavailable in the CLI —
# build the document one field at a time, re-reading the If-Match ETag before
# every command with `hoody projects proxy permissions get --project $PROJECT_ID -o json`.
hoody projects proxy groups ip set --project $PROJECT_ID \
--group-name developers --range 203.0.113.0/24 --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program terminal --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program display --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program files --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program http --access '[8080]' --if-match file:v<N>
hoody projects proxy default --project $PROJECT_ID --default deny --if-match file:v<N>
PATCH Configure IP-restricted developer access
/api/v1/projects/{project_id}/proxy/permissions
Click "Run" to execute the request

Result:

  • Developers from office network (203.0.113.0/24) get full access
  • Everyone else denied
  • Applies to all containers in project

Container-level override for public API:

Terminal window
# Public API (JWT for customers) + private admin group (password).
# Whole-document `permissions replace` is currently unavailable in the CLI —
# build the document one field at a time, re-reading the If-Match ETag before
# every command with `hoody containers proxy permissions get -c $CONTAINER_ID -o json`.
hoody containers proxy groups jwt set -c $CONTAINER_ID \
--group-name customers --secret 'a-long-random-signing-key-with-32-plus-chars' \
--algorithm HS256 --sources header:Authorization --if-match file:v<N>
hoody containers proxy groups password set -c $CONTAINER_ID \
--group-name admin --auth-username admin --auth-password 'hashed-admin-password' \
--salt 'unique-salt' --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name customers --program http --access '[8080]' --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name admin --program terminal --access true --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name admin --program display --access true --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name admin --program files --access true --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name admin --program http --access '[8080]' --if-match file:v<N>
hoody containers proxy default -c $CONTAINER_ID --default deny --if-match file:v<N>
PATCH Configure public API with private admin access
/api/v1/containers/{container_id}/proxy/permissions
Click "Run" to execute the request

Result:

  • Customers with valid JWT: HTTP API only
  • Admin with password: Full access (terminal, display, all files)
  • Everyone else: Denied

Different access levels for different teams:

Terminal window
# Multi-tier: ops (full), developers (partial), readonly (HTTP only).
# Whole-document `permissions replace` is currently unavailable in the CLI —
# build the document one field at a time, re-reading the If-Match ETag before
# every command with `hoody projects proxy permissions get --project $PROJECT_ID -o json`.
hoody projects proxy groups ip set --project $PROJECT_ID \
--group-name ops_team --range 203.0.113.0/24 --if-match file:v<N>
hoody projects proxy groups ip set --project $PROJECT_ID \
--group-name developers --range 198.51.100.0/24 --if-match file:v<N>
hoody projects proxy groups password set --project $PROJECT_ID \
--group-name readonly_users --auth-username viewer --auth-password 'hashed-pass' \
--salt 'salt' --if-match file:v<N>
# ops_team: terminal, display, files, http
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name ops_team --program terminal --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name ops_team --program display --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name ops_team --program files --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name ops_team --program http --access '[8080]' --if-match file:v<N>
# developers: terminal, files, http
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program terminal --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program files --access true --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name developers --program http --access '[8080]' --if-match file:v<N>
# readonly_users: http only
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name readonly_users --program http --access '[8080]' --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name readonly_users --program terminal --access false --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name readonly_users --program display --access false --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name readonly_users --program files --access false --if-match file:v<N>
hoody projects proxy default --project $PROJECT_ID --default deny --if-match file:v<N>
PATCH Configure multi-tier team access
/api/v1/projects/{project_id}/proxy/permissions
Click "Run" to execute the request

Access levels:

  • Ops team (203.0.113.0/24): Full access to all programs
  • Developers (198.51.100.0/24): Terminal, files, HTTP (no SSH, no display)
  • Read-only users (password auth): Only HTTP access

When both project and container permissions exist:

Request arrives at container service URL
Check: which permission documents exist?
BOTH → Merge project and container permissions
(container values win conflicts)
ONE → Use the one that exists
NEITHER → Default open (anyone with URL can access)

Example scenario:

Terminal window
# Project: Restrict all containers to office IP
PATCH /api/v1/projects/{id}/proxy/permissions
{
"groups": { "office": { "type": "ip", "range": "203.0.113.0/24" } },
"permissions": { "office": { "terminal": true, "http": [8080] } },
"default": "deny"
}
# Container: Override one container for public access
PATCH /api/v1/containers/{id}/proxy/permissions
{
"groups": { "public": { "type": "ip", "range": "0.0.0.0/0" } },
"permissions": { "public": { "http": [8080] } },
"default": "deny"
}

Result:

  • Most containers: Office-only access (terminal + HTTP)
  • Public container: Anyone can access HTTP
  • Public container: its own document lists only the public group — the office grant is not repeated there. Both documents govern a container, so read the project file and the container file together before assuming what a given request resolves to.

Terminal window
# Get current config (read the file_version ETag for If-Match)
hoody projects proxy permissions get --project $PROJECT_ID
# Set project permissions — whole-document `permissions replace` is currently
# unavailable in the CLI; use granular group/permission/default commands or
# the SDK/HTTP example.
hoody projects proxy groups ip set --project $PROJECT_ID \
--group-name <name> --range 203.0.113.0/24 --if-match file:v<N>
hoody projects proxy groups permissions set --project $PROJECT_ID \
--group-name <name> --program http --access '[8080]' --if-match file:v<N>
# Delete all permissions (revert to open)
hoody projects proxy permissions delete --project $PROJECT_ID --if-match file:v<N>
# Update default policy only
hoody projects proxy default --project $PROJECT_ID --if-match file:v<N> --default deny
# Enable the proxy (the CLI can only enable via this subcommand —
# --enable-proxy is a required, presence-only boolean flag; there is
# no --no-enable-proxy or --enable-proxy=false form)
hoody projects proxy state --project $PROJECT_ID --if-match file:v<N> --enable-proxy
# To disable the proxy, use the SDK or HTTP directly with enable_proxy: false
# (e.g. client.api.proxyPermissionsProject.updateState(id, { enable_proxy: false }, { ifMatch: 'file:v<N>' }))
Terminal window
# Get container config (read the file_version ETag for If-Match)
hoody containers proxy permissions get --container $CONTAINER_ID
# Set container permissions (override project). Whole-document `permissions replace`
# is currently unavailable in the CLI; use the granular CLI commands below, the
# granular SDK example, or the HTTP example for a whole-document replacement.
hoody containers proxy groups ip set -c $CONTAINER_ID \
--group-name <name> --range 203.0.113.0/24 --if-match file:v<N>
hoody containers proxy groups permissions set -c $CONTAINER_ID \
--group-name <name> --program http --access '[8080]' --if-match file:v<N>
# Delete container permissions (revert to project-level)
hoody containers proxy permissions delete --container $CONTAINER_ID --if-match file:v<N>
# Update default policy
hoody containers proxy default --container $CONTAINER_ID --if-match file:v<N> --default allow

Add/update/remove specific groups without replacing entire config:

Terminal window
# Add JWT group to project
hoody projects proxy groups jwt set --project $PROJECT_ID --group-name api-users \
--if-match file:v<N> \
--secret "a-long-random-signing-key-with-32-plus-chars" --algorithm HS256 --sources "header:Authorization"
# Add IP group to project
hoody projects proxy groups ip set --project $PROJECT_ID --group-name office \
--if-match file:v<N> --range "198.51.100.0/24"
# Remove group entirely
hoody projects proxy groups delete --project $PROJECT_ID --group-name office \
--if-match file:v<N>

Manage program permissions for groups:

Terminal window
# Set permissions for a group (boolean — all instances)
hoody projects proxy groups permissions set --project $PROJECT_ID --group-name developers \
--if-match file:v<N> --program terminal --access true
# Set permissions (specific instance)
hoody projects proxy groups permissions set --project $PROJECT_ID --group-name developers \
--if-match file:v<N> --program display --access 1
# Set permissions (multiple instances)
hoody projects proxy groups permissions set --project $PROJECT_ID --group-name developers \
--if-match file:v<N> --program terminal --access "[1,2,3]"
# Remove all permissions for a group
hoody projects proxy groups permissions clear --project $PROJECT_ID --group-name developers \
--if-match file:v<N>
# Remove specific program permission
hoody projects proxy groups permissions delete --project $PROJECT_ID --group-name developers \
--if-match file:v<N> --program terminal

Scenario 1: Open Development → Locked Production

Section titled “Scenario 1: Open Development → Locked Production”

Development phase (use cryptographic URLs):

No permissions configured
→ Anyone with URL can access
→ Share URLs with team for instant collaboration
→ Perfect for rapid iteration

Staging phase (add basic restrictions):

Terminal window
PATCH /api/v1/projects/{id}/proxy/permissions
{
"groups": {
"team": { "type": "ip", "range": "203.0.113.0/24" }
},
"permissions": {
"team": { "terminal": true, "http": [8080], "display": true }
},
"default": "deny"
}

Production phase (strict JWT auth):

Terminal window
# Override production container only
PATCH /api/v1/containers/{prod_id}/proxy/permissions
{
"groups": {
"customers": {
"type": "jwt",
"secret": "a-long-random-signing-key-with-32-plus-chars",
"algorithm": "HS256",
"sources": ["header:Authorization"]
}
},
"permissions": {
"customers": { "http": [8080] }
},
"default": "deny"
}

Result:

  • Dev containers: Open (cryptographic URLs)
  • Staging containers: Office IP only
  • Production container: JWT required

Give support team temporary terminal access:

Terminal window
# Add support group to specific container
PATCH /api/v1/containers/{id}/proxy/permissions/groups/support/password
{
"username": "support",
"password": "temporary-password-hash",
"algorithm": "sha256",
"salt": "salt"
}
# Grant specific access to support team
PATCH /api/v1/containers/{id}/proxy/permissions/permissions/support
{
"program": "terminal",
"access": 1 // Only terminal 1
}
PATCH /api/v1/containers/{id}/proxy/permissions/permissions/support
{
"program": "display",
"access": 1 // Only display 1
}
PATCH /api/v1/containers/{id}/proxy/permissions/permissions/support
{
"program": "files",
"access": true // All file instances
}

Support can:

  • Access terminal instance 1 only
  • View display instance 1 only
  • Use all file service instances

Instance control prevents accidental access to other terminals/displays used by your team.

After support session: Delete the support group or disable it.

Scenario 3: API Partners with Rate Limiting

Section titled “Scenario 3: API Partners with Rate Limiting”

Different token tiers for partners:

Terminal window
# Mutating /proxy/permissions requires an If-Match: file:v<N> header (ETag from a prior GET).
PATCH /api/v1/containers/{api_id}/proxy/permissions
{
"groups": {
"tier1_partners": {
"type": "token",
"value": "partner-abc-tier1",
"header": "X-Api-Token"
},
"tier2_partners": {
"type": "token",
"value": "partner-def-tier2",
"header": "X-Api-Token"
}
},
"permissions": {
"tier1_partners": {
"http": [8080], // All HTTP services
"files": true // All file instances
},
"tier2_partners": {
"http": [80, 3000], // Only HTTP on ports 80 and 3000
"files": false // No file access (files is boolean-only)
}
},
"default": "deny"
}

After setting permissions, test each group:

Terminal window
# Get current config
hoody projects proxy permissions get --project $PROJECT_ID
# Test access from your machine (for IP groups)
curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu"
# Test with Basic Auth (for password groups)
curl -u "username:password" \
"https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu"
# Test with JWT (for JWT groups)
curl -H "Authorization: Bearer eyJhbG..." \
"https://67e89abc...890abc-http-8080.node-us.containers.hoody.icu/api/endpoint"

If access is denied unexpectedly:

  1. Check group matches:

    Terminal window
    GET /api/v1/projects/{id}/proxy/permissions
    # Verify group exists and credentials/IP match
  2. Check program permissions:

    // Ensure the program is allowed for the group
    "permissions": {
    "yourgroup": {
    "terminal": true // Must be explicitly true
    }
    }
  3. Check default policy:

    "default": "deny" // If no group matches, deny
  4. Check container override:

    Terminal window
    GET /api/v1/containers/{id}/proxy/permissions
    # Container config might override project
  5. Check proxy enabled:

    "enable_proxy": true // Must be true

{
"default": "deny"
}

Why: Explicit allow is more secure than implicit allow. If you add new programs later, they’re denied by default until you explicitly permit them.

Grant minimum necessary access:

{
"permissions": {
"api_users": {
"http": [80], // Only HTTP on port 80 (public API)
"terminal": false, // No terminal access
"files": false, // No file access
"display": false, // No display access
"exec": false // No exec access
}
}
}

Combine multiple authentication methods:

{
"groups": {
"secure_access": {
"type": "ip",
"range": "203.0.113.0/24" // Matches users from the office network
},
"with_jwt": {
"type": "jwt",
"secret": "a-long-random-signing-key-with-32-plus-chars",
"algorithm": "HS256",
"sources": ["header:Authorization"] // Matches users with a valid JWT
}
}
}

Any matching group grants access. All groups are checked in order — a user from the office network matches secure_access; a user with a valid JWT matches with_jwt. Either path works independently. If you need a single group that requires both IP and a JWT simultaneously, that is not supported natively by the permissions model — use a proxy hook for that logic.

Terminal window
# List all project permissions
curl "https://api.hoody.icu/api/v1/projects" \
-H "Authorization: Bearer $HOODY_TOKEN"
# For each project, check permissions
curl "https://api.hoody.icu/api/v1/projects/{id}/proxy/permissions" \
-H "Authorization: Bearer $HOODY_TOKEN"
# Look for:
# - Overly broad IP ranges (0.0.0.0/0)
# - Expired access that should be removed
# - Groups no longer needed

Completely disable proxy for a project/container:

Terminal window
# The CLI cannot disable the proxy: --enable-proxy is a required,
# presence-only boolean flag (there is no --no-enable-proxy or
# --enable-proxy=false form). Use the SDK or HTTP tab with
# enable_proxy: false to disable at project or container level:
# client.api.proxyPermissionsProject.updateState($PROJECT_ID, { enable_proxy: false }, { ifMatch: 'file:v<N>' })
# client.api.proxyPermissionsContainer.updateState($CONTAINER_ID, { enable_proxy: false }, { ifMatch: 'file:v<N>' })
# Re-enable
hoody containers proxy state --container $CONTAINER_ID --if-match file:v<N> --enable-proxy

When disabled:

  • enable_proxy: false is a kill-switch, not a bypass. New requests that reach the proxy’s permission layer are denied with 403 — the proxy answers itself instead of forwarding to your service
  • The check runs before authentication groups, permission rules, and the default policy, so no configured rule can re-open access while the switch is off
  • The container keeps running, and access that never traverses the proxy (for example SSH) is unaffected. Established WebSocket and CONNECT tunnels are not torn down — the gate applies to new connections. Compiled config is cached for a couple of seconds, so a flip is not instantaneous
  • Re-enable to restore group/permission enforcement

Scope — read this before disabling at project level. The container-level setting wins over the project-level one in both directions: a container document with enable_proxy: true stays reachable even while its project is set to false. This is not a corner case — the API writes enable_proxy: true explicitly whenever a container permissions document is saved, so most containers carry an explicit true. To reliably disable a specific container, set it at container level.


{
"project": "string (required, project ID)",
"container": "string (optional, for container-level only)",
"groups": {
"{groupName}": {
"type": "jwt" | "password" | "ip" | "token" | "hoody-identity",
// ... type-specific fields
}
},
"permissions": {
"{groupName}": {
// Network programs (terminal, ssh, display, http): boolean, a port/instance
// number, an array of numbers, a port-range string like "8000-8100", or "*"
"terminal": true | false | number | [number] | "8000-8100" | "*",
"display": true | false | number | [number] | "8000-8100" | "*",
"http": [8080] | 8080 | "8000-8100", // must NAME ports — true, false, and "*" are rejected on write
// Boolean-only programs (files, exec, services, notifications)
"files": true | false,
"exec": true | false,
"services": true | false,
"notifications": true | false
}
},
"default": "allow" | "deny",
"enable_proxy": true | false // optional, defaults to true
}

enable_proxy is an optional field of the permissions document body (it defaults to true and is persisted in the document’s settings alongside default). To flip it without rewriting the whole document, use the dedicated proxy state/settings endpoint instead (PATCH .../proxy/permissions/state or PATCH .../proxy/settings).

JWT:

{
"type": "jwt",
"secret": "string (required)",
"algorithm": "HS256 | RS256 | ES256 (required)",
"sources": ["string"] (required, e.g., ["header:Authorization"]),
"claims": {} (optional, required JWT claims)
}

Password:

{
"type": "password",
"username": "string (required)",
"password": "string (required, plain or hashed)",
"algorithm": "sha256 (required — only sha256 is accepted)",
"salt": "string (required)"
}

IP:

{
"type": "ip",
"range": "string (required, IPv4 CIDR)"
}

Token:

{
"type": "token",
"value": "string (required, the token to match)",
"header": "string" | "cookie": "string" | "param": "string"
// exactly one of header | cookie | param specifying where the token is read from
}

Hoody Identity:

{
"type": "hoody-identity",
"audience": "string (required, the claim's aud must equal this exactly)",
"sources": ["string"] (optional, defaults to ["header:X-Hoody-Identity-Claim"]),
"allow_types": ["user" | "admin"] (optional, defaults to both),
"users": ["string"] (optional, allowlist of up to 256 user IDs),
"max_age_seconds": 86400 (optional, freshness gate on the claim's iat, minimum 300),
"expose_type": false (optional, also inject X-Hoody-Identity-Type)
}

Details and caveats: Hoody Identity Authentication.


Can I use multiple authentication methods for the same group?

Section titled “Can I use multiple authentication methods for the same group?”

No. Each group uses exactly one authentication type (JWT, password, IP, token, or hoody-identity). However, you can create multiple groups with different auth methods and all will be checked. If any group matches, the user gets access with that group’s permissions.

Do container-level permissions merge with project-level permissions?

Section titled “Do container-level permissions merge with project-level permissions?”

Yes. Container-level permissions merge with project-level permissions. Project entries remain effective unless an overlapping container group or permission overrides them; the container default takes precedence when present.

What happens if no permissions are configured at all?

Section titled “What happens if no permissions are configured at all?”

The container is open by default - anyone with the URL can access all services. This is intentional for rapid development and instant collaboration. The cryptographic URL (2^192 combinations) provides security through obscurity.

Can I restrict access to specific HTTP ports?

Section titled “Can I restrict access to specific HTTP ports?”

Yes! Use instance numbers for the http program. For example, "http": [80, 3000] allows only ports 80 and 3000. Each port is treated as an instance.

How do I temporarily disable access to a container?

Section titled “How do I temporarily disable access to a container?”

Use the proxy state endpoint:

Terminal window
PATCH /api/v1/containers/{id}/proxy/permissions/state
{ "enable_proxy": false }

New requests to that container’s proxied service URLs then return 403 until you re-enable it. This is a hard cut, not a bypass: the switch is evaluated before groups, rules, and the default policy. The container keeps running, and access that does not traverse the proxy (for example SSH) still works. Set it at container level — a project-level false does not override a container whose own document says true.

Can IP authentication work with dynamic IPs?

Section titled “Can IP authentication work with dynamic IPs?”

IP auth requires static IPs or CIDR ranges. For dynamic IPs, use JWT or token authentication instead, or combine IP with a VPN that provides static exit IPs.

What’s the difference between “default”: “allow” and no permissions?

Section titled “What’s the difference between “default”: “allow” and no permissions?”
  • No permissions configured: Open by default, no auth required
  • “default”: “allow”: If groups exist but none match, still allow access
  • “default”: “deny”: If groups exist but none match, deny access

Use "default": "deny" for security when you have authentication groups.

Not through the proxy permissions system directly. For access logging, use:

  • Container firewall logs for connection attempts
  • Service-level logging (terminal, exec, etc. all support logging)
  • MITM via hoody-exec to log all HTTP traffic
  1. Add new group with new secret/tokens
  2. Update client applications to use new credentials
  3. Verify new group works
  4. Delete old group: DELETE /api/v1/projects/{id}/proxy/permissions/groups/{oldGroupName}

Can password authentication use bcrypt or argon2?

Section titled “Can password authentication use bcrypt or argon2?”

Currently only SHA256 is supported for password hashing. For stronger auth, use JWT with a proper authentication service.


Your proxy is now secure:

  1. Authentication configured - Groups define who can access
  2. Permissions set - Programs define what they can do
  3. Default policy chosen - Deny by default for security

Explore related security:


Default: Open by cryptographic URL.
Project-level: Consistent team access.
Container-level: Production security.
Complete control over who accesses what.

From zero-config collaboration to enterprise-grade security—all through HTTP.