Policies are the heart of Housecarl's authorization system. They define who can do what to which resources. This guide covers everything you need to know about creating, managing, and testing policies.
A policy is a set of rules that determine whether an authorization request should be allowed or denied. Think of it as a bouncer at a club who checks if you meet the criteria to get in.
Every policy has these key components:
name = "engineering-team-access"
description = "Allow engineering team to access project resources"
engine = "RegEx"
deny = false
invert = false
[[statements]]
group = "engineering"
action = "(read|write)"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/projects/.*/engineering/.*"
name - Unique identifier for the policy within its domain. Use descriptive names that explain what the policy does.
description (optional) - Human-readable explanation of the policy's purpose. Future you will thank present you for writing this.
engine - How pattern matching works. Choose from: Fixed, Prefix, Glob, or RegEx. More on this below.
deny - Set to true for deny policies (explicitly block access), false for allow policies. Default is false.
invert - Flip the evaluation result. Rarely used. Default is false.
statements - Array of rule sets. Each statement is a set of conditions that must ALL match for the policy to apply.
The evaluation engine determines how Housecarl matches patterns in your policy against incoming requests. Choosing the right engine is crucial for both functionality and performance.
Use when: You need exact string equality with no pattern matching.
Performance: Fastest - simple string comparison.
Example:
name = "alice-only"
engine = "Fixed"
deny = false
invert = false
[[statements]]
subject = "alice"
action = "admin"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/system/admin-panel"
This matches ONLY when the subject is exactly "alice", action is exactly "admin", and object is exactly "hc://domain/550e8400-e29b-41d4-a716-446655440000/system/admin-panel". No wildcards, no patterns.
When to use Fixed:
Use when: You have hierarchical paths and want to match everything under a prefix.
Performance: Fast - simple string prefix check.
Example:
name = "docs-hierarchy"
engine = "Prefix"
deny = false
invert = false
[[statements]]
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/public/"
This matches any object that starts with "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/public/" - so public/readme.txt, public/specs/design.doc, public/images/logo.png all match. Omitting subject means no constraint on who the subject is — any subject can read.
When to use Prefix:
Use when: You need flexible pattern matching with wildcards but want path-awareness.
Performance: Good - uses efficient dynamic programming algorithm (no ReDoS risk).
Wildcards:
* - Matches zero or more characters, BUT NOT /? - Matches exactly one character, BUT NOT /Example:
name = "team-docs-pattern"
engine = "Glob"
deny = false
invert = false
[[statements]]
group = "engineering"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/*/engineering/*"
This matches:
hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/2024/engineering/roadmap.md ✓hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/shared/engineering/specs.pdf ✓hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/engineering/plans.txt ✗ (needs something between documents and engineering)Important: Glob wildcards DON'T cross / boundaries. This is a security feature that prevents overly broad matches.
When to use Glob:
*@example.com)Use when: You need advanced pattern matching features like character classes, alternation, or quantifiers.
Performance: Slower - full regex evaluation.
Example:
name = "multi-region-access"
engine = "RegEx"
deny = false
invert = false
[[statements]]
subject = "svc:.*"
action = "read|write|delete"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/storage/(us|eu|ap)-[a-z]+-[0-9]+/.*"
This matches service accounts accessing storage in specific region formats like us-west-1, eu-central-2, ap-south-3.
When to use RegEx:
[a-z0-9](apple|banana|cherry)){3,5}Warning: With great power comes great responsibility. Complex regex patterns can be hard to understand and maintain. Use the simpler engines unless you really need regex features.
There are three ways to create policies in Housecarl: through the CLI, through the web UI, and programmatically via the gRPC API. We'll focus on the CLI here as it's the most common for administration.
The most maintainable approach is to keep your policies in version control as TOML files.
Step 1: Create a policy file:
cat > engineering-read.toml <<'EOF'
name = "engineering-read-access"
description = "Engineering team members can read all project documentation"
engine = "Glob"
deny = false
invert = false
[[statements]]
group = "engineering"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/projects/*"
EOF
Step 2: Add it to a domain:
housectl domain put-policies <domain-uuid> engineering-read.toml
For multiple policies, organize them in a directory:
mkdir policies/
# Create multiple policy files
cat > policies/engineering-read.toml <<'EOF'
name = "engineering-read-access"
description = "Engineering team can read project docs"
engine = "Glob"
deny = false
invert = false
[[statements]]
group = "engineering"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/projects/*"
EOF
cat > policies/engineering-write.toml <<'EOF'
name = "engineering-write-access"
description = "Engineering team can write to project docs"
engine = "Glob"
deny = false
invert = false
[[statements]]
group = "engineering"
action = "write"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/projects/*"
EOF
# Deploy all at once
housectl domain put-policies <domain-uuid> policies/
Note: put-policies is atomic - either all policies are deployed or none are. This prevents partial updates.
A statement contains multiple rules. For the statement to match, ALL rules must match. This complete policy shows the AND logic:
name = "engineering-read-for-product"
engine = "Glob"
deny = false
invert = false
[[statements]]
group = "engineering"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/projects/*"
department = "product"
This matches only when:
A policy can have multiple statements. If ANY statement matches, the policy matches. This complete policy shows the OR logic:
name = "admin-or-engineering-read"
engine = "RegEx"
deny = false
invert = false
[[statements]]
role = "admin"
action = ".*"
object = "hc://domain/[a-f0-9-]{36}/.*"
[[statements]]
group = "engineering"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/engineering/.*"
This allows access if:
Grant access based on user roles:
name = "admin-full-access"
engine = "RegEx"
deny = false
invert = false
[[statements]]
role = "admin"
action = ".*"
object = "hc://domain/[a-f0-9-]{36}/.*"
name = "viewer-read-only"
engine = "RegEx"
deny = false
invert = false
[[statements]]
role = "viewer"
action = "read"
object = "hc://domain/[a-f0-9-]{36}/.*"
Allow access to everything under a path:
name = "team-folder-access"
engine = "Glob"
deny = false
invert = false
[[statements]]
group = "platform-team"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/shared/platform/*"
[[statements]]
group = "platform-team"
action = "write"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/shared/platform/*"
Let users manage their own resources using macros:
name = "user-own-profile"
engine = "Fixed"
deny = false
invert = false
[[statements]]
subject = "$current_user()"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/user/$current_user()/profile"
[[statements]]
subject = "$current_user()"
action = "write"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/user/$current_user()/profile"
The $current_user() macro expands to the authenticated username at evaluation time. Alice can only access her own profile, Bob can only access his.
Use context attributes for fine-grained control:
name = "classified-docs-clearance"
engine = "Glob"
deny = false
invert = false
[[statements]]
clearance = "top-secret"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/classified/*"
Only users whose authorization request includes clearance: top-secret in the context can access classified documents.
Restrict access to business hours (requires your application to set the time attribute):
name = "contractor-business-hours"
engine = "Prefix"
deny = false
invert = false
[[statements]]
account_type = "contractor"
business_hours = "true"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/systems/production/"
Your application would set business_hours: true only during allowed times.
Explicitly block access (overrides allow policies):
name = "deny-sensitive-api"
engine = "Prefix"
deny = true
invert = false
[[statements]]
account_type = "external"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/api/internal/"
Even if other policies allow it, external accounts are denied access to internal APIs.
Always test your policies before deploying to production. Housecarl provides several testing approaches.
Test policies entirely offline:
# Create a test request
cat > test-request.json <<'EOF'
{
"context": {
"subject": "alice",
"action": "read",
"object": "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/projects/alpha/spec.md",
"group": "engineering"
}
}
EOF
# Test against local policy file
housectl authz can-i-local --request test-request.json engineering-read.toml
Expected output:
ALLOW
Test policies against the server without deploying them:
housectl authz can-i-maybe test-request.json engineering-read.toml
This evaluates using both the deployed server policies AND your local test policy.
Check policy syntax without execution:
housectl authz parse-policies engineering-read.toml
This is great for CI/CD pipelines:
# In your CI pipeline
housectl authz parse-policies policies/
if [ $? -ne 0 ]; then
echo "Policy validation failed!"
exit 1
fi
After deployment, test with real requests using a test user:
# Create test request for a real authorization check
housectl authz can-i test-request.json
This queries the actual deployed policies on the server.
See all policies in a domain:
housectl domain list-policies my-domain
Example output:
POLICY NAME ENGINE DENY STATEMENTS
engineering-read-access Glob false 1
admin-full-access RegEx false 1
deny-external-api Prefix true 1
Retrieve the full policy definition:
housectl domain get-policy my-domain engineering-read-access
To update a policy, create a new version of the TOML file and re-deploy:
# Edit the policy file
vim engineering-read.toml
# Validate the syntax
housectl authz parse-policies engineering-read.toml
# Deploy the updated policy
housectl domain put-policies <domain-uuid> engineering-read.toml
Important: Policy updates take effect immediately. All subsequent authorization requests will use the new policy.
Remove a policy from a domain:
# Put an empty set of policies (removes all)
mkdir -p empty-policies
housectl domain put-policies <domain-uuid> empty-policies/
# Or deploy only the policies you want to keep
housectl domain put-policies <domain-uuid> policy1.toml policy2.toml
Understanding how Housecarl evaluates policies helps you write effective policies:
Example: You have these policies:
Request: External engineer reads internal document
Macros are dynamic placeholders that get expanded at evaluation time:
| Macro | Expands To | Use Case |
|---|---|---|
$current_user() | Authenticated username | User-specific resources |
$requestors_tenant() | Caller's tenant ID | Tenant isolation checks |
$current_time() | Unix timestamp | Time-based access |
$resource_tenant() | Resource's tenant ID | Cross-tenant validation |
Example with macros:
name = "user-own-resources"
engine = "Glob"
deny = false
invert = false
[[statements]]
subject = "$current_user()"
action = "read"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/user/$current_user()/*"
[[statements]]
subject = "$current_user()"
action = "write"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/user/$current_user()/*"
[[statements]]
subject = "$current_user()"
action = "delete"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/user/$current_user()/*"
When Alice makes a request, $current_user() becomes "alice", so she can only access hc://domain/550e8400-e29b-41d4-a716-446655440000/user/alice/*.
Step 1: Check if the policy is actually deployed:
housectl domain list-policies my-domain
Step 2: Verify the policy content:
housectl domain get-policy my-domain my-policy-name
Step 3: Test locally to see what's matching:
housectl authz can-i-local --request test.json my-policy.toml
Step 4: Check for typos in patterns. Pattern matching is EXACT - the pattern hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/project-alpha/* matches files inside project-alpha/ but NOT files in project-alpha-staging/ or project-alphabeta/. The prefix before the wildcard must match exactly.
Step 5: For Glob patterns, remember wildcards don't cross /:
hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/* matches hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/readme.txt ✓hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/* does NOT match hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/specs/design.md ✗hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/*/* or hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/.* (with RegEx engine)Use a more restrictive engine or more specific patterns:
Too broad (Glob engine):
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/*"
This matches hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/anything.txt but won't recurse.
More specific:
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/public/*.txt"
This matches only .txt files directly in the public folder.
Even more specific (Fixed engine):
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/public/readme.txt"
This matches only that exact file.
Use local testing:
housectl authz can-i-local --request test.json new-policy.toml
Or use a separate development tenant:
housectl config login https://housecarl.cloud me@example.com --tenant my-dev-tenant
housectl domain put-policies <test-domain-uuid> new-policy.toml
housectl authz can-i test.json
Keep policy TOML files in git:
policies/
├── admin-access.toml
├── engineering-team.toml
├── contractor-read-only.toml
└── deny-sensitive-api.toml
Good: engineering-team-read-project-docs
Bad: policy1
name = "contractor-access"
description = "Contractors can read public docs during business hours. Added 2024-01-15 for vendor access project. See ticket #123."
engine = "Prefix"
deny = false
invert = false
Begin with minimal access and add permissions as needed. It's easier to grant than revoke.
Always test policy changes in a dev environment before production.
Create a doc explaining your policy strategy:
Review your policies quarterly:
Deny policies are powerful but can be confusing. Use them only for:
Use domain put-policies to deploy all policies at once. This ensures consistency:
# Good: atomic deployment (use your domain UUID)
housectl domain put-policies <domain-uuid> policies/
# Avoid: piecemeal updates that could leave things in inconsistent state
| Need | Engine | Pattern Example |
|---|---|---|
| Exact match | Fixed | hc://domain/550e8400-e29b-41d4-a716-446655440000/system/admin |
| All items under path | Prefix | hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/public/ |
| File patterns | Glob | hc://domain/550e8400-e29b-41d4-a716-446655440000/documents/*/*.pdf |
| Email domains | Glob | *@example.com |
| Complex patterns | RegEx | hc://domain/550e8400-e29b-41d4-a716-446655440000/storage/(us|eu)-\w+-[0-9]+/.* |
| OR logic within rule | RegEx | action: "read|write|delete" |
| User's own resources | Any | Use $current_user() macro |
| Deny access | Any | Set "deny": true |
Ready to write some policies? Check out the Policy Cookbook for patterns you can copy and adapt!