Skip to main content

Authorization (OPA)

Honey can enforce policy-as-code authorization with Open Policy Agent (OPA). Instead of a single shared token that grants all-or-nothing access, you write Rego policies that decide — per actor, per host, per command — what may run.

OPA is opt-in: with no policy configured, Honey behaves exactly as before. The engine runs embedded (in-process), so there is no sidecar to deploy.

Enabling​

Point Honey at a directory of .rego files (package honey) when starting the web server:

export HONEY_POLICY_DIR=/etc/honey/policies
honey web

Every policy lives in package honey and sets allow (and optionally deny_reason, decision, requires). A minimal allow-all default ships embedded; your directory overrides it.

package honey
import rego.v1

default allow := false
default deny_reason := ""

# allow read-only API traffic
allow if input.action == "api_request"

Actor identity​

Decisions need to know who is acting. Honey resolves the actor for each authenticated request in this order:

  1. Trusted-proxy header — X-Honey-User, honored only when the request's peer is in HONEY_TRUSTED_PROXIES (CSV of CIDRs / IPs). This is the Grafana X-WEBAUTH-USER pattern: an auth proxy (Caddy, oauth2-proxy, Authelia) authenticates the user and injects the header.
  2. JWT — an Ed25519-signed bearer token whose sub claim is the actor. Enable with HONEY_JWT_PUBLIC_KEY (base64 Ed25519 public key).
  3. Fallback — api (the shared-token caller, which carries no identity).
export HONEY_TRUSTED_PROXIES="10.0.0.0/8,192.168.0.0/16"
export HONEY_JWT_PUBLIC_KEY="$(base64 < ed25519_pub.raw)"

JWT end-to-end​

Generate an Ed25519 keypair, hand Honey the public key, and sign tokens whose sub claim is the actor:

# 1. keypair (raw 32-byte public key → base64)
openssl genpkey -algorithm ed25519 -out ed25519.pem
openssl pkey -in ed25519.pem -pubout -outform DER \
| tail -c 32 | base64 > ed25519_pub.b64

# 2. tell Honey the public key
export HONEY_JWT_PUBLIC_KEY="$(cat ed25519_pub.b64)"
honey web

# 3. call the API with a signed bearer token (sub → actor)
curl -H "Authorization: Bearer $JWT" $HONEY/api/v1/recipes

The token's sub (e.g. alice@example.com) becomes input.actor for every decision. An invalid or expired signature is rejected; the request then falls back to the shared-token api actor only if it also carries the shared token.

Webhook runs resolve their actor from a trusted header/JWT, else the recipe's webhook.actor (a gjson path into the payload, e.g. sender.login), else webhook:<app>.

Declare actor per-webhook in the recipe — it is a gjson path into the incoming payload:

recipe: {
name: "deploy-on-push"
webhooks: {
"github_push": {
auth_secret: "env:GH_WEBHOOK_SECRET"
actor: "pusher.name" // gjson path → input.actor
extract: {COMMIT: "after"}
}
}
steps: [{host: "web-*", command: "deploy.sh \(env.COMMIT)"}]
}
# payload {"pusher":{"name":"alice"},"after":"abc123"} → actor "alice"
# route is /api/v1/webhooks/{app_name}/{webhook_name}
curl -XPOST -H "Authorization: $GH_WEBHOOK_SECRET" \
-d '{"pusher":{"name":"alice"},"after":"abc123"}' \
$HONEY/api/v1/webhooks/my-app/github_push

A trusted X-Honey-User header or a JWT on the request overrides the gjson actor; if neither is present and there is no actor path, the actor falls back to webhook:<app_name> — the app registry name from the URL (webhook:my-app for the route above), not the recipe's name.

Decision points​

A single enforcer is consulted at several actions. Your policy branches on input.action:

input.actionWhenKey input fields
api_requestEvery authenticated /api/v1/* requestactor, method, path
recipe_executeBefore a recipe runs (admission)actor, recipe, hosts, execution
step_executePer host, before a step's commandactor, step_kind, host, host_meta, host_vars
command_execPer command/script, with risk signalsactor, command, target, execution (see Command Risk)
interactive_sessionBefore opening a web SSH shellactor, target
recipe_approveWhen approving a held runapprover, requester, recipe

step_execute filters the host list: denied hosts are skipped (and shown as skipped in the run output) while allowed hosts proceed.

Inventory as policy data​

Your config inventory is exposed to policies as the OPA data document data.inventory (global vars, CEL groups, per-host hosts). Additionally, each host's resolved inventory variables are passed as input.target.host_vars at step_execute / command_exec, so a policy can gate on a host's effective tier:

allow := false if {
input.action == "command_exec"
input.command.max_severity == "high"
input.target.host_vars.tier == "prod"
}

Approval flow​

A policy may return decision := "require_approval" instead of a hard deny. The run is then held: the API responds 202 {status:"pending_approval", id} rather than executing.

decision := "require_approval" if {
input.action == "recipe_execute"
not input.execution.approved
}
allow if { input.action == "recipe_execute"; input.execution.approved }

An authorized actor approves it:

curl -XPOST $HONEY/api/v1/approvals/$ID -d '{"decision":"approve"}'

The approval is itself gated by recipe_approve (e.g. require the approver to differ from the requester):

allow if {
input.action == "recipe_approve"
input.approver != input.requester
}

Re-submitting the run with the approval_id then proceeds. Pending runs live in an in-memory store with a 24h TTL; list them with GET /api/v1/approvals.

Biometric step-up (WebAuthn)​

For the highest-risk operations a policy can demand a fresh passkey assertion:

decision := "require_biometric" if {
input.action == "recipe_execute"
input.target.env == "prod"
}
allow if { input.action == "recipe_execute"; input.execution.biometricVerified }

Enable WebAuthn and the /api/v1/webauthn/* register/assert endpoints:

export HONEY_WEBAUTHN_RPID=honey.example.com
export HONEY_WEBAUTHN_ORIGIN=https://honey.example.com
export HONEY_WEBAUTHN_SECRET=<random-32-bytes> # signs biometric tokens

After a successful assertion the client receives a short-lived biometric_token, replayed as the X-Honey-Biometric header on the require_biometric run.

End-to-end, a prod run gated by the policy above:

# 1. run a prod recipe → held, server asks for a passkey
curl -XPOST $HONEY/api/v1/cue-exec \
-d '{"recipe_path":"deploy.cue","execute":true,"records":[...]}'
# → 202 {"status":"require_biometric"}

# 2. assert the passkey (begin → sign in the browser/authenticator → finish)
curl -XPOST $HONEY/api/v1/webauthn/assert/begin -d '{"actor":"alice@example.com"}'
curl -XPOST $HONEY/api/v1/webauthn/assert/finish -d '<signed assertion>'
# → {"biometric_token":"<token>"}

# 3. re-run with the token in the header → input.execution.biometricVerified
curl -XPOST $HONEY/api/v1/cue-exec \
-H "X-Honey-Biometric: <token>" \
-d '{"recipe_path":"deploy.cue","execute":true,"records":[...]}'

The token is short-lived and single-purpose — a fresh assertion is required for each require_biometric run.

In-recipe policy checks (opa step)​

Recipes can evaluate a policy inline and fail/branch on the result:

steps: [
{
host: "_"
opa: { policy: "policies/compliance.rego" }
},
]

The step compiles the referenced .rego (relative to the recipe dir), evaluates it with {actor, recipe, ...custom input}, and fails the step when allow == false — usable with when / depends to gate later steps.

A graph recipe where a compliance check gates the deploy — the deploy step only runs once gate succeeds:

recipe: {
name: "guarded-deploy"
type: "graph"
steps: [
{
id: "gate"
host: "_"
opa: {
policy: "policies/compliance.rego"
input: {change_ticket: "CHG-1024", window: "maintenance"}
}
},
{
id: "deploy"
host: "web-*"
depends: ["gate"]
command: "deploy.sh"
},
]
}
# policies/compliance.rego
package honey
import rego.v1

default allow := false

allow if {
input.change_ticket != ""
input.window == "maintenance"
}

Environment variables​

VariableEffect
HONEY_POLICY_DIRDirectory of .rego files; enables all OPA gates
HONEY_JWT_PUBLIC_KEYbase64 Ed25519 public key; enables JWT identity
HONEY_TRUSTED_PROXIESCSV of CIDRs/IPs trusted to assert X-Honey-User
HONEY_WEBAUTHN_RPID / _ORIGIN / _SECRETenable passkey biometric step-up

See Command Risk Engine for the deterministic command analysis that feeds the command_exec decision.