Open Policy Agent

Evaluate Virtual Signer approval requests with tested Rego policy

The Virtual Signer directly supports Open Policy Agent (OPA) through its external-policy contract. Requests already use OPA's {"input": ...} envelope and OPA returns the required {"result": ...} response through its Data API.

The examples on this page were executed with OPA 1.17.0. Pin the OPA version approved for your io.sign release rather than using a floating image tag.

OPA's Data API uses a different URL shape, so place a TLS adapter in front of OPA:

Virtual Signer Broker
  POST /v2/approve/sign
          |
          v
TLS adapter and path rewrite
  /v2/approve/{operation} -> /v1/data/policy/approve/{operation}
          |
          v
OPA Data API
  data.policy.approve.sign

Configure the Broker

IOSIGN_SIGNER_BROKER_APPROVAL_MODE=api-approval
IOSIGN_SIGNER_BROKER_APPROVAL_EXTERNAL_POLICY_BASE_URL=https://opa-policy.example.internal/v2/approve
IOSIGN_SIGNER_BROKER_APPROVAL_EXTERNAL_POLICY_TIMEOUT=5
IOSIGN_SIGNER_BROKER_APPROVAL_POLICY_CA_CERT_FILE=/run/secrets/policy-ca.pem

The Broker appends /sign, /keygen, or /reshare. The adapter rewrites that path to the corresponding rule under /v1/data/policy/approve/ and forwards the body unchanged.

Start with a fail-closed policy

Save this as approve.rego. It rejects every operation by default. The example approves a batch signing request only when it targets the allowed vault and every signature is a Polygon transfer no greater than the configured amount.

package policy.approve

import rego.v1

default sign := {"decision": "reject"}
default keygen := {"decision": "reject"}
default reshare := {"decision": "reject"}

allowed_vault := "vault-456"
polygon_chain_id := "137"
max_polygon_wei := 4000000000000000

sign := {"decision": "approve"} if {
    input.type == "sign"
    input.vaultId == allowed_vault
    count(input.signatures) > 0

    every signature in input.signatures {
        signature.metadata.contextType == "application/evm-rlp-v1+hex"
        signature.metadata.decoded.parsed.ChainID == polygon_chain_id
        to_number(signature.metadata.decoded.parsed.Amount) <= max_polygon_wei
    }
}

The every check is important because one approval request can contain multiple signatures. Missing metadata, an unexpected chain, an excessive amount, or an empty batch leaves the default rejection in place.

Test the policy

Save this as approve_test.rego beside the policy:

package policy.approve_test

import rego.v1
import data.policy.approve

approved_input := {
    "type": "sign",
    "vaultId": "vault-456",
    "signatures": [{
        "metadata": {
            "contextType": "application/evm-rlp-v1+hex",
            "decoded": {"parsed": {"ChainID": "137", "Amount": "1000"}}
        }
    }]
}

test_approved_transfer if {
    result := approve.sign with input as approved_input
    result.decision == "approve"
}

test_rejects_excessive_transfer if {
    excessive := object.union(
        approved_input,
        {"signatures": [{
            "metadata": {
                "contextType": "application/evm-rlp-v1+hex",
                "decoded": {"parsed": {"ChainID": "137", "Amount": "5000000000000000"}}
            }
        }]},
    )
    result := approve.sign with input as excessive
    result.decision == "reject"
}

test_rejects_keygen_by_default if {
    result := approve.keygen with input as {"type": "keygen", "vaultId": "vault-456"}
    result.decision == "reject"
}

Run all discovered tests and fail if none are found. See OPA's policy-testing reference for test discovery and additional options.

opa test --fail-on-empty approve.rego approve_test.rego

Configure a TLS path adapter

This Caddy route illustrates the required rewrite. Supply production-managed certificates and restrict OPA's port to the adapter.

policy.example.internal:8443 {
    tls /certs/server.pem /certs/server-key.pem

    handle_path /v2/approve/* {
        rewrite * /v1/data/policy/approve{uri}
        reverse_proxy opa:8181
    }
}

OPA evaluates data.policy.approve.sign, keygen, or reshare and wraps the rule value inside result, which matches the Broker response contract.

Production guidance

  • Pin an approved OPA image version; do not deploy an unpinned latest tag.
  • Keep OPA's policy-management and Data API ports private to the adapter and administration plane.
  • Manage Rego and its tests in version control with review required for policy changes.
  • Do not enable raw decision logs in production without redaction. Inputs can contain transaction details and public-key material.
  • Monitor evaluation latency and errors below the Broker timeout.
  • Add explicit key-generation and resharing rules only after defining which vault membership and threshold changes are allowed.

See Custom policy server for the common wire contract and failure behavior.


Did this page help you?