Proxy Permissions
Section titled “Proxy Permissions”Every request to a container service passes through the Hoody Proxy, which checks it against your permission configuration. You decide who can access which services, per group and per instance, at project or container level.
This builds on the proxy architecture and aliases.
API endpoints summary
Section titled “API endpoints summary”This Foundation page explains permission concepts and configuration strategies. The complete endpoint documentation:
Project-level permissions:
- GET /api/v1/projects/{id}/proxy/permissions - Get project permissions
- PUT /api/v1/projects/{id}/proxy/permissions - Set project permissions
- DELETE /api/v1/projects/{id}/proxy/permissions - Remove project permissions
- PATCH /api/v1/projects/{id}/proxy/permissions/default - Update default policy
- PATCH /api/v1/projects/{id}/proxy/permissions/state - Toggle permissions on/off
- PATCH /api/v1/projects/{id}/proxy/permissions/groups/{name}/ip - Add/update IP auth group
- PATCH /api/v1/projects/{id}/proxy/permissions/groups/{name}/jwt - Add/update JWT auth group
- PATCH /api/v1/projects/{id}/proxy/permissions/groups/{name}/password - Add/update password auth group
- PATCH /api/v1/projects/{id}/proxy/permissions/groups/{name}/token - Add/update token auth group
- DELETE /api/v1/projects/{id}/proxy/permissions/groups/{name} - Remove auth group
- PATCH /api/v1/projects/{id}/proxy/permissions/permissions/{name} - Set group program permissions
- DELETE /api/v1/projects/{id}/proxy/permissions/permissions/{name} - Remove group permissions
- DELETE /api/v1/projects/{id}/proxy/permissions/permissions/{name}/{program} - Remove per-program permission
Container-level permissions:
- GET /api/v1/containers/{id}/proxy/permissions - Get container permissions
- PUT /api/v1/containers/{id}/proxy/permissions - Set container permissions (overrides project)
- DELETE /api/v1/containers/{id}/proxy/permissions - Remove container permissions
- PATCH /api/v1/containers/{id}/proxy/permissions/state - Toggle container permissions on/off
- PATCH /api/v1/containers/{id}/proxy/permissions/default - Update container default policy
- DELETE /api/v1/containers/{id}/proxy/permissions/groups/{name} - Remove specific group
- PATCH /api/v1/containers/{id}/proxy/permissions/groups/{name}/ip - Add/update container IP auth group
- PATCH /api/v1/containers/{id}/proxy/permissions/groups/{name}/jwt - Add/update container JWT auth group
- PATCH /api/v1/containers/{id}/proxy/permissions/groups/{name}/password - Add/update container password auth group
- PATCH /api/v1/containers/{id}/proxy/permissions/groups/{name}/token - Add/update container token auth group
- PATCH /api/v1/containers/{id}/proxy/permissions/permissions/{name} - Set group program permissions
- DELETE /api/v1/containers/{id}/proxy/permissions/permissions/{name} - Remove group permissions
- DELETE /api/v1/containers/{id}/proxy/permissions/permissions/{name}/{program} - Remove per-program permission
Container proxy hooks (MITM traffic interception):
- GET /api/v1/containers/{id}/proxy/hooks - List all hooks grouped by service
- POST /api/v1/containers/{id}/proxy/hooks/{service} - Append or insert a hook
- PATCH /api/v1/containers/{id}/proxy/hooks/{service}/{hookId} - Replace a hook
- DELETE /api/v1/containers/{id}/proxy/hooks/{service}/{hookId} - Remove a hook
- PATCH /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}/position - Move a hook
- See Proxy Hooks for the full endpoint list and semantics.
Container proxy settings (root enable/default policy):
- GET /api/v1/containers/{id}/proxy/settings - Get
enable_proxyanddefault - PUT /api/v1/containers/{id}/proxy/settings - Update
enable_proxyand/ordefault
The permission system
Section titled “The permission system”Access control happens in layers:
┌─────────────────────────────────────┐│ Hoody API Authentication │ ← User login, API tokens│ (api.hoody.com) │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ 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 are involved:
- Hoody API auth - Access to platform management (create containers, configure firewall)
- 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.
Core concepts
Section titled “Core concepts”Groups (who can access)
Section titled “Groups (who can access)”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)
For 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 (what they can access)
Section titled “Permissions (what they can access)”Permissions map groups to programs, with per-instance control.
Each group gets specific access to container programs using:
true- Allow all instancesfalse- Deny all instancesnumber- Allow only this instance/port (e.g.,1allows 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 forhttp/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.
Explicit ports for http and https
Section titled “Explicit ports for http and https”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 name the ports it opens:
"http": [8080] // this port only"http": [80, 8080] // these ports"http": 8080 // shorthand for one port"http": "8000-8100" // an inclusive rangeA 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). A document that already contains "http": false still acts as an explicit deny at the proxy.
What a port grant opens
Section titled “What a port grant opens”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 } }}Default policy (fallback)
Section titled “Default policy (fallback)”The default policy decides 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)
Hierarchy (project vs container)
Section titled “Hierarchy (project vs container)”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 are configured, the two documents merge and container-level values take precedence wherever they overlap.
Open by default
Section titled “Open by default”A new container has no proxy permissions configured. Its URLs are reachable by anyone who has them: the proxy applies no authentication and no group matching, and the two 24-character IDs in the hostname are the credential.
The URL contains:
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.com └────────24-char hex────┘ └────────24-char hex────┘The project and container IDs give 2^96 × 2^96 = 2^192 possible combinations, which makes the pair practically unguessable. Sharing the URL grants access, and so does any other way the URL travels: pasted into Slack, captured in a screenshot, logged by a referer header, or cached by a browser extension, it lets the holder hit the container directly. Unguessability prevents blind discovery; it does not contain leaks or replace access control.
This default is what makes instant collaboration work: sharing the URL is the whole setup, nothing needs configuring, and anyone who has the link can join.
When you need real access control, set proxy permissions before sharing anything sensitive. Do not rely on URL secrecy, and never paste container URLs into public channels, public dashboards, or untrusted third-party tools.
Configure permissions
Section titled “Configure permissions”Project-level permissions
Section titled “Project-level permissions”To apply authentication to every container in a project:
# 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>await client.api.proxyPermissionsProject.replace(PROJECT_ID, { project: PROJECT_ID, groups: { team: { type: 'ip', range: '203.0.113.0/24' } }, permissions: { team: { terminal: [1, 2], files: true, display: 1, http: [8080] } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "groups": { "team": { "type": "ip", "range": "203.0.113.0/24" } }, "permissions": { "team": { "terminal": [1, 2], "files": true, "display": 1, "http": [8080] } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Sets project-level proxy permissions in one request. The If-Match value must be the document’s current file_version ETag, so re-GET the document first if <N> is stale.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","groups":{"team":{"type":"ip","range":"203.0.113.0/24"}},"permissions":{"team":{"terminal":[1,2],"files":true,"display":1,"http":[8080]}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Every container in this project now:
- requires a client IP in the
203.0.113.0/24range - grants that group access to terminal, files, display, and http
- denies all other access
Container-level permissions
Section titled “Container-level permissions”To override the project settings for a specific container:
# 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>await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { public: { type: 'ip', range: '0.0.0.0/0' } }, permissions: { public: { http: [8080], terminal: false, files: false } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "container": "'$CONTAINER_ID'", "groups": { "public": { "type": "ip", "range": "0.0.0.0/0" } }, "permissions": { "public": { "http": [8080], "terminal": false, "files": false } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Overrides the project’s permissions for this one container in a single request, opening HTTP while denying terminal and files. The If-Match value must be the document’s current file_version ETag.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","container":"CONTAINER_ID","groups":{"public":{"type":"ip","range":"0.0.0.0/0"}},"permissions":{"public":{"http":[8080],"terminal":false,"files":false}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
This container now:
- accepts requests from any IP (public access)
- allows HTTP services only
- denies terminal and files for the
publicgroup - overrides the project document only where entries overlap; project groups this document does not name stay in force
A typical use: a public demo container in an otherwise private project.
Authentication groups
Section titled “Authentication groups”A group defines how a client authenticates.
JWT authentication
Section titled “JWT authentication”A jwt group validates 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). ForHS256the key must be at least 32 characters and must not contain a common weak word (secret,password,123456,test,dev,default); the API returns422otherwise. ForRS256/ES256this is the PEM-encoded public key.algorithm- HS256, RS256, or ES256 (required)sources- Where to look for the token (required). Each entry must matchheader:Nameorcookie:Name; JWTs can only be read from a header or a cookie.claims- Required JWT claims to validate (optional)
Token source examples:
header:Authorization- Bearer token in the Authorization headercookie:session- JWT in a session cookie
Use this when your application issues JWTs to users and Hoody validates them at the proxy.
Hoody identity authentication
Section titled “Hoody identity authentication”A hoody-identity group gates a service on “a valid Hoody user”. 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:
audience- Required. The claim’saudmust equal this exactly. Clients mint a bound claim withPOST /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); enforces “my users only” at the proxy.max_age_seconds- Optional freshness gate on the claim’siat(min 300).expose_type- Also injectX-Hoody-Identity-Type(user|admin). Off by default so platform-admin isn’t mistaken for app-admin.
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-* headers. 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.
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 that anonymous access is denied: a curl without the header must get 403. Once a gate file has been active, a missing or undistributed file fails closed (deny), never open.
In v1 the gate covers programmatic clients: CLIs, SDKs, native apps, and server-to-server calls. Browsers cannot attach custom headers on navigation, so use a proxy hook for browser apps. It applies on the reverse-proxy path only (not the forward-proxy/CONNECT products) and gates WebSocket handshakes; established sockets outlive claim expiry, so terminate server-side if you need live cutoff.
Password authentication
Section titled “Password authentication”A password group uses HTTP Basic Auth:
{ "groups": { "admin_access": { "type": "password", "username": "admin", "password": "hashed-password-here", "algorithm": "sha256", "salt": "unique-salt-value" } }}Parameters:
username- Exact username match requiredpassword- Plain or hashed passwordalgorithm- Hashing algorithm (sha256)salt- Salt for password hashing
Browsers show their built-in Basic Auth prompt automatically.
Basic Auth is an old protocol, and that is its value here: it is portable. It needs no JWT library or token management, every browser has a native prompt, and any HTTP client can send it with nothing but a username and password. Credentials are simple enough for a person to remember and type, and they can be embedded in a URL (https://user:pass@domain.com works in most contexts). There is no token expiration, refresh flow, or claims validation to manage, and it keeps working when an external OAuth provider is down.
Common uses:
- Simple admin access
- Quick demos and prototypes
- Temporary contractor access
- Emergency access paths
- Internal tools where JWT overhead isn’t worth it
- Any scenario where portability matters more than sophistication
IP-based authentication
Section titled “IP-based authentication”An ip group allows or denies 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/32for a single IP)
Use this to restrict access to an office network, a VPN, or known IPs.
The proxy sees real client IPs (not proxy IPs) thanks to Hoody’s netfilter hooks, so IP rules match the actual client.
Token authentication
Section titled “Token authentication”A token group validates 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 this to distribute tokens to API consumers, partners, and integrations.
Authentication without headers (hoody-curl)
Section titled “Authentication without headers (hoody-curl)”Some environments cannot send custom headers: browser bookmarks (just URLs), QR codes (GET only), email links (no header control), and restricted platforms (iOS Shortcuts, some automation tools). hoody-curl works around this by transforming an authenticated request into a plain GET URL.
How it works:
# Requires ability to send Authorization headercurl "https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com/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# hoody-curl wraps the request as a GET URLhttps://PROJECT_ID-CONTAINER_ID-curl-1.node-us.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com/execute&method=POST&header=Authorization: Bearer jwt-token&json={"command":"ls -la"}
# Works as browser bookmark# Works as QR code# Can be clicked from email# Works in restricted environmentsThe hoody-curl service:
- Receives GET request with parameters
- Constructs proper POST request with headers
- Sends to target service
- Returns response
For example, a bookmark that triggers a deployment:
// Create a bookmark URL that deploys your appconst curlService = "https://PROJECT_ID-CONTAINER_ID-curl-1.node-us.containers.hoody.com";const targetService = "https://PROJECT_ID-CONTAINER_ID-exec-1.node-us.containers.hoody.com/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"// Clicking the bookmark triggers the deployment// without a terminal or a curl commandUse cases:
- Emergency deployments - A bookmark that deploys
- Mobile access - A QR code that triggers a workflow
- Email notifications - “Click here to approve” links
- Restricted automation - iOS Shortcuts, Zapier webhooks
- Simple sharing - Send a URL instead of a curl command
See Hoody cURL for complete documentation on wrapping HTTP requests.
Per-program permissions
Section titled “Per-program permissions”Every program takes an access rule, and most of them can name individual instances instead of the whole program.
Access rule value types
Section titled “Access rule value types”A permission cell holds a boolean, a number, an array, a port-range string, or a wildcard:
{ "permissions": { "developers": { "terminal": true, // Allow ALL terminal instances "files": false // Deny ALL file service instances } }}Use when: Simple all-or-nothing access
{ "permissions": { "customers": { "display": 1, // Allow ONLY display instance 1 "terminal": 2 // Allow ONLY terminal instance 2 } }}Use when: Grant access to one specific instance
{ "permissions": { "team": { "terminal": [1, 2, 3], // Allow terminal instances 1, 2, and 3 "display": [1], // Allow only UI instance 1 "http": [80, 3000] // Allow HTTP ports 80 and 3000 } }}Use when: Grant access to specific subset of instances
{ "permissions": { "developers": { "terminal": [1, 2], // Terminals 1 and 2 only "files": true, // All file instances "display": 1, // Only display 1 "http": [8080], // All HTTP services "exec": false // No exec access } }}Use when: Complex access patterns
Available programs
Section titled “Available programs”The programs you can configure:
http- HTTP services (port-based:http-80,http-3000, etc.)ssh- SSH access to containerterminal- Hoody Terminal service instancesdisplay- Hoody Display (desktop / web-UI) instancesfiles- Hoody Files serviceexec- Hoody Exec script executionservices- Container service endpointsnotifications- Notification service
Instance-level isolation
Section titled “Instance-level isolation”A container running five terminal instances can give each team its own:
{ "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 reaches only its own instances, and all three share one container.
Configuration examples
Section titled “Configuration examples”Development team access
Section titled “Development team access”An internal team with broad access, restricted to the office IP range:
# 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>await client.api.proxyPermissionsProject.replace(PROJECT_ID, { project: PROJECT_ID, groups: { developers: { type: 'ip', range: '203.0.113.0/24' } }, permissions: { developers: { terminal: true, display: true, files: true, http: [8080] } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "groups": { "developers": { "type": "ip", "range": "203.0.113.0/24" } }, "permissions": { "developers": { "terminal": true, "display": true, "files": true, "http": [8080] } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Configures the office-IP-restricted developer group in one request. The If-Match value must be the document’s current file_version ETag.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","groups":{"developers":{"type":"ip","range":"203.0.113.0/24"}},"permissions":{"developers":{"terminal":true,"display":true,"files":true,"http":[8080]}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
With this configuration:
- developers from the office network (203.0.113.0/24) get full access
- everyone else is denied
- the rules apply to every container in the project
Public API with private admin
Section titled “Public API with private admin”A container-level override that opens the HTTP API while keeping the admin services behind a password:
# 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>await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { 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-admin-password', algorithm: 'sha256', salt: 'unique-salt' } }, permissions: { customers: { http: [8080] }, admin: { terminal: true, display: true, files: true, http: [8080] } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "container": "'$CONTAINER_ID'", "groups": { "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-admin-password", "algorithm": "sha256", "salt": "unique-salt" } }, "permissions": { "customers": { "http": [8080] }, "admin": { "terminal": true, "display": true, "files": true, "http": [8080] } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Writes both groups and both permission grants in one request: customers get HTTP-only JWT access, admin gets full password-gated access. The If-Match value must be the document’s current file_version ETag.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","container":"CONTAINER_ID","groups":{"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-admin-password","algorithm":"sha256","salt":"unique-salt"}},"permissions":{"customers":{"http":[8080]},"admin":{"terminal":true,"display":true,"files":true,"http":[8080]}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
With this configuration:
- customers with a valid JWT reach the HTTP API only
- the admin group authenticates with a password and reaches terminal, display, and all files
- everyone else is denied
Multi-tier team access
Section titled “Multi-tier team access”Three teams on one project document, each with a different level of access:
# 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, httphoody 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, httphoody 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 onlyhoody 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>await client.api.proxyPermissionsProject.replace(PROJECT_ID, { project: PROJECT_ID, groups: { ops_team: { type: 'ip', range: '203.0.113.0/24' }, developers: { type: 'ip', range: '198.51.100.0/24' }, readonly_users: { type: 'password', username: 'viewer', password: 'hashed-pass', algorithm: 'sha256', salt: 'salt' } }, permissions: { ops_team: { terminal: true, display: true, files: true, http: [8080] }, developers: { terminal: true, files: true, http: [8080] }, readonly_users: { http: [8080], terminal: false, display: false, files: false } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# The If-Match ETag (file:v<N>) comes from a prior GET of the permissions document.curl -X PUT "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{ "project": "'$PROJECT_ID'", "groups": { "ops_team": { "type": "ip", "range": "203.0.113.0/24" }, "developers": { "type": "ip", "range": "198.51.100.0/24" }, "readonly_users": { "type": "password", "username": "viewer", "password": "hashed-pass", "algorithm": "sha256", "salt": "salt" } }, "permissions": { "ops_team": { "terminal": true, "display": true, "files": true, "http": [8080] }, "developers": { "terminal": true, "files": true, "http": [8080] }, "readonly_users": { "http": [8080], "terminal": false, "display": false, "files": false } }, "default": "deny" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Writes all three tiers in one request: full access for ops, terminal/files/HTTP for developers, HTTP-only for read-only users. The If-Match value must be the document’s current file_version ETag.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions&method=PUT&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"project":"PROJECT_ID","groups":{"ops_team":{"type":"ip","range":"203.0.113.0/24"},"developers":{"type":"ip","range":"198.51.100.0/24"},"readonly_users":{"type":"password","username":"viewer","password":"hashed-pass","algorithm":"sha256","salt":"salt"}},"permissions":{"ops_team":{"terminal":true,"display":true,"files":true,"http":[8080]},"developers":{"terminal":true,"files":true,"http":[8080]},"readonly_users":{"http":[8080],"terminal":false,"display":false,"files":false}},"default":"deny"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
The three groups resolve to:
- Ops team (203.0.113.0/24): every program
- Developers (198.51.100.0/24): terminal, files, and HTTP; no SSH and no display
- Read-only users (password auth): HTTP only
Permission hierarchy
Section titled “Permission hierarchy”When both documents exist, the proxy resolves a request like this:
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)For example, a project restricted to the office IP, with one container opened for public HTTP:
# Project: Restrict all containers to office IPPATCH /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 accessPATCH /api/v1/containers/{id}/proxy/permissions{ "groups": { "public": { "type": "ip", "range": "0.0.0.0/0" } }, "permissions": { "public": { "http": [8080] } }, "default": "deny"}With both documents in place:
- most containers: office-only access (terminal and HTTP)
- public container: anyone can reach HTTP
- public container: its own document lists only the
publicgroup, and theofficegrant 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.
Configuration endpoints
Section titled “Configuration endpoints”Project-level operations
Section titled “Project-level operations”# 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 onlyhoody 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>' }))// Get current config. The GET returns an envelope: { statusCode, message, data }const { data } = await client.api.proxyPermissionsProject.get(PROJECT_ID);
// Set project permissions: send only the writable fields, guarded by the ETag you just read.// schema_version, file_version and etag are read-only and are rejected in the body.await client.api.proxyPermissionsProject.replace(PROJECT_ID, { project: PROJECT_ID, groups: data.groups ?? {}, permissions: data.permissions ?? {}, default: data.default, enable_proxy: data.enable_proxy}, { ifMatch: data.etag });
// Delete all permissions (revert to open)await client.api.proxyPermissionsProject.delete(PROJECT_ID, { ifMatch: 'file:v<N>' });
// Update default policy onlyawait client.api.proxyPermissionsProject.updateDefault(PROJECT_ID, { default: 'deny' }, { ifMatch: 'file:v<N>' });# Get current configcurl "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN"
# Set project permissions (If-Match ETag from the GET above)curl -X PUT "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{...}'
# Delete all permissions (revert to open)curl -X DELETE "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "If-Match: file:v<N>"
# Update default policy onlycurl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/default" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"default": "deny"}'
# Enable/disable proxy entirelycurl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/state" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"enable_proxy": false}'Container-level operations
Section titled “Container-level operations”# 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 policyhoody containers proxy default --container $CONTAINER_ID --if-match file:v<N> --default allow// Get container config. The GET returns an envelope: { statusCode, message, data }const { data } = await client.api.proxyPermissionsContainer.get(CONTAINER_ID);
// Set one program's access rule (override project). A granular write leaves// stored hooks untouched. The bulk `replace` body cannot carry the hooks the// GET returned: their server-assigned `id` is rejected by the body schema, and// stripping it fails the service's own `id is required` check.await client.api.proxyPermissionsContainer.setGroup( CONTAINER_ID, '<name>', { program: 'http', access: 8080 }, { ifMatch: data.etag });
// Delete container permissions (revert to project-level)await client.api.proxyPermissionsContainer.delete(CONTAINER_ID, { ifMatch: 'file:v<N>' });
// Update default policyawait client.api.proxyPermissionsContainer.updateDefault(CONTAINER_ID, { default: 'allow' }, { ifMatch: 'file:v<N>' });# Get container configcurl "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN"
# Set container permissions (override project; If-Match ETag from the GET above)curl -X PUT "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{...}'
# Delete container permissions (revert to project-level)curl -X DELETE "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN" \ -H "If-Match: file:v<N>"
# Update default policycurl -X PATCH "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions/default" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"default": "allow"}'Group operations
Section titled “Group operations”Add, update, or remove a single group without replacing the whole document:
# Add JWT group to projecthoody 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 projecthoody projects proxy groups ip set --project $PROJECT_ID --group-name office \ --if-match file:v<N> --range "198.51.100.0/24"
# Remove group entirelyhoody projects proxy groups delete --project $PROJECT_ID --group-name office \ --if-match file:v<N>// Add JWT group to projectawait client.api.proxyPermissionsProject.setJwtGroup(PROJECT_ID, 'api-users', { secret: 'a-long-random-signing-key-with-32-plus-chars', algorithm: 'HS256', sources: ['header:Authorization']}, { ifMatch: 'file:v<N>' });
// Add IP groupawait client.api.proxyPermissionsProject.setIpGroup(PROJECT_ID, 'office', { range: '198.51.100.0/24' }, { ifMatch: 'file:v<N>' });
// Remove groupawait client.api.proxyPermissionsProject.removeAuthGroup(PROJECT_ID, 'office', { ifMatch: 'file:v<N>' });# Add JWT group to projectcurl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/groups/api-users/jwt" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"secret": "a-long-random-signing-key-with-32-plus-chars", "algorithm": "HS256", "sources": ["header:Authorization"]}'
# Add IP groupcurl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/groups/office/ip" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"range": "198.51.100.0/24"}'
# Remove group entirelycurl -X DELETE "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/groups/office" \ -H "Authorization: Bearer $TOKEN" \ -H "If-Match: file:v<N>"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Each group write or removal as its own link. Every one needs the current file_version ETag in If-Match, so re-GET the document before using a link if <N> is stale.
# Add JWT group
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/groups/api-users/jwt&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"secret":"a-long-random-signing-key-with-32-plus-chars","algorithm":"HS256","sources":["header:Authorization"]}&response=transparent
# Add IP group
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/groups/office/ip&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"range":"198.51.100.0/24"}&response=transparent
# Remove group entirely
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/groups/office&method=DELETE&bearer_token=TOKEN&header=If-Match:%20file:v<N>&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Permission operations
Section titled “Permission operations”Set or remove a group’s program permissions:
# 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 grouphoody projects proxy groups permissions clear --project $PROJECT_ID --group-name developers \ --if-match file:v<N>
# Remove specific program permissionhoody projects proxy groups permissions delete --project $PROJECT_ID --group-name developers \ --if-match file:v<N> --program terminal// Set permissions (all terminal instances)await client.api.proxyPermissionsProject.setGroup(PROJECT_ID, 'developers', { program: 'terminal', access: true }, { ifMatch: 'file:v<N>' });
// Set permissions (specific instance)await client.api.proxyPermissionsProject.setGroup(PROJECT_ID, 'developers', { program: 'display', access: 1 }, { ifMatch: 'file:v<N>' });
// Set permissions (multiple instances)await client.api.proxyPermissionsProject.setGroup(PROJECT_ID, 'developers', { program: 'terminal', access: [1, 2, 3] }, { ifMatch: 'file:v<N>' });
// Remove all permissions for a groupawait client.api.proxyPermissionsProject.removeGroup(PROJECT_ID, 'developers', { ifMatch: 'file:v<N>' });# Set permissions for a group (boolean)curl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"program": "terminal", "access": true}'
# Set permissions (specific instance)curl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"program": "display", "access": 1}'
# Set permissions (multiple instances)curl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"program": "terminal", "access": [1, 2, 3]}'
# Remove all permissions for a groupcurl -X DELETE "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \ -H "Authorization: Bearer $TOKEN" \ -H "If-Match: file:v<N>"
# Remove specific program permissioncurl -X DELETE "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers/terminal" \ -H "Authorization: Bearer $TOKEN" \ -H "If-Match: file:v<N>"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Each link carries its own If-Match: replace file:v<N> with the
file_version the GET returned, or the write is rejected. The first three share
one URL and differ only by body.
# Set permissions for a group (boolean)
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/permissions/developers&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"program":"terminal","access":true}&response=transparent
# Set permissions (specific instance)
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/permissions/developers&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"program":"display","access":1}&response=transparent
# Set permissions (multiple instances)
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/permissions/developers&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"program":"terminal","access":[1,2,3]}&response=transparent
# Remove all permissions for a group
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/permissions/developers&method=DELETE&bearer_token=TOKEN&header=If-Match:%20file:v<N>&response=transparent
# Remove specific program permission
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/permissions/developers/terminal&method=DELETE&bearer_token=TOKEN&header=If-Match:%20file:v<N>&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Operational scenarios
Section titled “Operational scenarios”Progressive lockdown from dev to production
Section titled “Progressive lockdown from dev to production”Development phase. No permissions are configured, so anyone holding a URL can reach the container and the team shares URLs directly.
Staging phase. Restrict the whole project to the office IP range:
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):
# Override production container onlyPATCH /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
Customer support access
Section titled “Customer support access”Give support team temporary terminal access:
# Add support group to specific containerPATCH /api/v1/containers/{id}/proxy/permissions/groups/support/password{ "username": "support", "password": "temporary-password-hash", "algorithm": "sha256", "salt": "salt"}
# Grant specific access to support teamPATCH /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.
API partners with rate limiting
Section titled “API partners with rate limiting”Different token tiers for partners:
# 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"}Permission Testing
Section titled “Permission Testing”Verify Configuration
Section titled “Verify Configuration”After setting permissions, test each group:
# Get current confighoody projects proxy permissions get --project $PROJECT_ID
# Test access from your machine (for IP groups)curl "https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com"
# Test with Basic Auth (for password groups)curl -u "username:password" \ "https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com"
# Test with JWT (for JWT groups)curl -H "Authorization: Bearer eyJhbG..." \ "https://PROJECT_ID-CONTAINER_ID-http-8080.node-us.containers.hoody.com/api/endpoint"// Get current config to verifyconst config = await client.api.proxyPermissionsProject.get(PROJECT_ID);console.log(JSON.stringify(config.data, null, 2));
// Test access programmatically via container clientconst containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER});const result = await containerClient.terminal.execution.execute({ command: 'echo "access works"'});# Get current configcurl "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions" \ -H "Authorization: Bearer $TOKEN"
# Test with IP group (from allowed IP)curl "https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com"
# Test with password group (Basic Auth)curl "https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com" \ -u "username:password"
# Test with JWT group (Bearer token)curl "https://PROJECT_ID-CONTAINER_ID-http-8080.node-us.containers.hoody.com/api/endpoint" \ -H "Authorization: Bearer eyJhbG..."
# From denied IP or without auth (should return 401/403)curl "https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Reads the document, then probes the target service with Basic Auth and with a JWT. Expect 401/403 unless the document’s default is allow or a group matches — note these run from the curl container, not from your machine. The IP-group probe above (anonymous, from your own machine) is not reproducible as a link: routed through hoody-curl, it would test the curl container’s IP instead of yours, so run it from a terminal as shown in the CLI/HTTP tabs.
# Get current permissions config
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions&method=GET&bearer_token=TOKEN&response=transparent
# Basic Auth request (password group)
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-terminal-1.node-us.containers.hoody.com&method=GET&header=Authorization:%20Basic%20dXNlcm5hbWU6cGFzc3dvcmQ=&response=transparent
# JWT request (JWT group)
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-http-8080.node-us.containers.hoody.com/api/endpoint&method=GET&bearer_token=eyJhbG...&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Debugging access issues
Section titled “Debugging access issues”If access is denied unexpectedly:
-
Check group matches:
Terminal window GET /api/v1/projects/{id}/proxy/permissions# Verify group exists and credentials/IP match -
Check program permissions:
// Ensure the program is allowed for the group"permissions": {"yourgroup": {"terminal": true // Must be explicitly true}} -
Check default policy:
"default": "deny" // If no group matches, deny -
Check container override:
Terminal window GET /api/v1/containers/{id}/proxy/permissions# Container config might override project -
Check proxy enabled:
"enable_proxy": true // Must be true
Security best practices
Section titled “Security best practices”Start with default deny
Section titled “Start with default deny”{ "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.
Use least privilege
Section titled “Use least privilege”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 } }}3. Layer Security
Section titled “3. Layer Security”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.
Audit permissions regularly
Section titled “Audit permissions regularly”# List all project permissionscurl "https://api.hoody.com/api/v1/projects" \ -H "Authorization: Bearer $HOODY_TOKEN"
# For each project, check permissionscurl "https://api.hoody.com/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 neededDisabling the proxy
Section titled “Disabling the proxy”Completely disable proxy for a project/container:
# 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-enablehoody containers proxy state --container $CONTAINER_ID --if-match file:v<N> --enable-proxy// Disable at project levelawait client.api.proxyPermissionsProject.updateState(PROJECT_ID, { enable_proxy: false }, { ifMatch: 'file:v<N>' });
// Disable at container levelawait client.api.proxyPermissionsContainer.updateState(CONTAINER_ID, { enable_proxy: false }, { ifMatch: 'file:v<N>' });
// Re-enableawait client.api.proxyPermissionsContainer.updateState(CONTAINER_ID, { enable_proxy: true }, { ifMatch: 'file:v<N>' });# Disable at project level (applies only to containers that don't set enable_proxy themselves)curl -X PATCH "https://api.hoody.com/api/v1/projects/$PROJECT_ID/proxy/permissions/state" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"enable_proxy": false}'
# Disable at container level (this container's service URLs then return 403)curl -X PATCH "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"enable_proxy": false}'
# Re-enablecurl -X PATCH "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"enable_proxy": true}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Flips the kill-switch at project or container level, and re-enables it. These
links target PROJECT_ID and CONTAINER_ID, so route them through a different
project’s running container (OTHER_PROJECT_ID/OTHER_CONTAINER_ID above) —
the second link disables the proxy for CONTAINER_ID itself, which would sever
the transport for the third link if it ran through the same container or
project. Each write needs the document’s current file_version ETag in
If-Match.
# Disable at project level
https://OTHER_PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/proxy/permissions/state&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"enable_proxy":false}&response=transparent
# Disable at container level
https://OTHER_PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions/state&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"enable_proxy":false}&response=transparent
# Re-enable at container level
https://OTHER_PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/proxy/permissions/state&method=PATCH&bearer_token=TOKEN&header=If-Match:%20file:v<N>&json={"enable_proxy":true}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
When disabled:
enable_proxy: falseis a kill-switch, not a bypass. New requests that reach the proxy’s permission layer are denied with403, because the proxy answers itself instead of forwarding to your service- The check runs before authentication groups, permission rules, and the
defaultpolicy, 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, because 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, and 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.
Permission configuration reference
Section titled “Permission configuration reference”Full configuration structure
Section titled “Full configuration structure”{ "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_proxyis an optional field of the permissions document body (it defaults totrueand is persisted in the document’s settings alongsidedefault). To flip it without rewriting the whole document, use the dedicated proxy state/settings endpoint instead (PATCH .../proxy/permissions/stateorPATCH .../proxy/settings).
Authentication type fields
Section titled “Authentication type fields”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.
Useful Questions
Section titled “Useful Questions”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:
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, because 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.
Can I see who accessed my containers?
Section titled “Can I see who accessed my containers?”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
How do I rotate JWT secrets or tokens?
Section titled “How do I rotate JWT secrets or tokens?”- Add new group with new secret/tokens
- Update client applications to use new credentials
- Verify new group works
- 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.
What’s Next
Section titled “What’s Next”The proxy now enforces:
- Authentication configured - Groups define who can access
- Permissions set - Programs define what they can do
- Default policy chosen - Deny by default for security
Explore related security:
- Container Firewall → - Network-level rules (ingress/egress)
- Container Network → - Proxy/VPN routing
- IPv4 Management → - Dedicated IP addresses
The three tiers cover the usual progression: a new container is reachable by its unguessable URL, project-level rules give a team consistent access across containers, and container-level rules lock down what reaches production.
Permissions scale from open collaboration to strict access control, all through the same HTTP layer.