Policy Administration
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.
Understanding 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.
Anatomy of a Policy
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.
Choosing an Evaluation Engine
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.
Fixed Engine (Exact Match)
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:
- Service account access to specific endpoints
- Exact resource identifiers
- Hard-coded access rules
Prefix Engine (Starts-With Matching)
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:
- Resource hierarchies (all items under a folder)
- API endpoint prefixes (all /api/v1/... endpoints)
- Namespace-based access
Glob Engine (Wildcard Patterns)
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:
- File-like path patterns
- Email domains (
*@example.com) - Flexible resource matching with safety constraints
RegEx Engine (Regular Expressions)
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:
- Complex patterns that require character classes
[a-z0-9] - Alternation beyond simple OR (
(apple|banana|cherry)) - Quantifiers for precise counts
{3,5} - When Glob isn't expressive enough
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.
Creating Policies
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.
Method 1: Create Policy Files
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
Method 2: Directory of Policies
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.
Policy Statements: The AND Rule
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:
- group is "engineering" AND
- action is "read" AND
- object matches the pattern AND
- department is "product"
Multiple Statements: The OR Rule
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:
- User is an admin (first statement) OR
- User is in engineering group reading engineering docs (second statement)
Common Policy Patterns
Pattern 1: Role-Based Access Control (RBAC)
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}/.*"
Pattern 2: Resource Hierarchies
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/*"
Pattern 3: Self-Service Resources
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.
Pattern 4: Attribute-Based Access Control (ABAC)
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.
Pattern 5: Time-Based Access
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.
Pattern 6: Deny Policies
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.
Testing Policies
Always test your policies before deploying to production. Housecarl provides several testing approaches.
Option 1: Local Testing (No Server)
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
Option 2: Speculative Server Testing
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.
Option 3: Parse and Validate
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
Option 4: Production Testing (Safe)
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.
Managing Policies
Listing Policies
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
Getting a Specific Policy
Retrieve the full policy definition:
housectl domain get-policy my-domain engineering-read-access
Updating Policies
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.
Deleting Policies
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
Policy Evaluation Order
Understanding how Housecarl evaluates policies helps you write effective policies:
- Collect policies: Gather all policies from the request's domain and all superior domains
- Evaluate each policy: For each policy, check if any statement matches the request
- Combine results: Apply these rules in order:
- If ANY deny policy matches → DENIED (deny wins)
- If at least one allow policy matches → ALLOWED
- If NO policy matches → DENIED (secure by default)
Example: You have these policies:
- Policy A (allow): Engineering can read documents
- Policy B (deny): External accounts cannot access internal docs
Request: External engineer reads internal document
- Policy A matches (engineer reading docs) → ALLOW
- Policy B matches (external account + internal docs) → DENY
- Result: DENIED (deny wins)
Available Macros
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/*.
Troubleshooting Policies
"Why is my request denied?"
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/*matcheshc://domain/550e8400-e29b-41d4-a716-446655440000/docs/readme.txt✓hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/*does NOT matchhc://domain/550e8400-e29b-41d4-a716-446655440000/docs/specs/design.md✗- Use
hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/*/*orhc://domain/550e8400-e29b-41d4-a716-446655440000/docs/.*(with RegEx engine)
"My policy matches too much"
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.
"How do I test without affecting production?"
Use local testing:
housectl authz can-i-local --request test.json new-policy.toml
Or use a separate development tenant:
housectl config login https://api.authz.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
Best Practices
1. Version Control Your Policies
Keep policy TOML files in git:
policies/
├── admin-access.toml
├── engineering-team.toml
├── contractor-read-only.toml
└── deny-sensitive-api.toml
2. Use Descriptive Names
Good: engineering-team-read-project-docs
Bad: policy1
3. Write Comments in Descriptions
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
4. Start Restrictive, Then Loosen
Begin with minimal access and add permissions as needed. It's easier to grant than revoke.
5. Use the Simplest Engine That Works
- Need exact match? Use Fixed
- Need prefix? Use Prefix
- Need wildcards? Use Glob
- Need advanced patterns? Use RegEx
6. Test in Development First
Always test policy changes in a dev environment before production.
7. Document Your Authorization Model
Create a doc explaining your policy strategy:
- What domains exist and their purpose
- What roles/groups you use
- Common patterns in your policies
- Who can create/modify policies
8. Regular Policy Audits
Review your policies quarterly:
- Are there unused policies?
- Are there overly broad policies?
- Do policies still match business requirements?
- Can any policies be simplified?
9. Use Deny Policies Sparingly
Deny policies are powerful but can be confusing. Use them only for:
- Security-critical blocks (prevent external access to internal systems)
- Overriding inherited allows from superior domains
- Temporary access revocation
10. Atomic Deployments
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
Next Steps
- Policy Cookbook: See Policy Cookbook for more real-world examples
- Domain Organization: Learn about hierarchy and inheritance in Tenant & Domain Policy
- API Integration: Integrate authorization checks into your app with Housecarl API
- Operational Guidance: Change management and rollouts in Update Procedures
Policy Quick Reference
| 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!