PRE-RELEASE INFORMATION: SUBJECT TO CHANGE

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:

  1. Collect policies: Gather all policies from the request's domain and all superior domains
  2. Evaluate each policy: For each policy, check if any statement matches the request
  3. 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:

MacroExpands ToUse Case
$current_user()Authenticated usernameUser-specific resources
$requestors_tenant()Caller's tenant IDTenant isolation checks
$current_time()Unix timestampTime-based access
$resource_tenant()Resource's tenant IDCross-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/* 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
  • Use hc://domain/550e8400-e29b-41d4-a716-446655440000/docs/*/* or hc://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://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 Quick Reference

NeedEnginePattern Example
Exact matchFixedhc://domain/550e8400-e29b-41d4-a716-446655440000/system/admin
All items under pathPrefixhc://domain/550e8400-e29b-41d4-a716-446655440000/documents/public/
File patternsGlobhc://domain/550e8400-e29b-41d4-a716-446655440000/documents/*/*.pdf
Email domainsGlob*@example.com
Complex patternsRegExhc://domain/550e8400-e29b-41d4-a716-446655440000/storage/(us|eu)-\w+-[0-9]+/.*
OR logic within ruleRegExaction: "read|write|delete"
User's own resourcesAnyUse $current_user() macro
Deny accessAnySet "deny": true

Ready to write some policies? Check out the Policy Cookbook for patterns you can copy and adapt!