Skip to content

Policy

The ezoidc server must be configured with a policy written in Rego v1.

The policy must define cases for the allow.read and allow.internal functions to implement the access control rules for the variables. The body of each function case should verify the issuer, subject, and claims of the token to ensure the client is authorized to access the variable.

Functions

allow.read(name)

Determines if the variable name can be read.

policy.rego
# only allow "api_key"
allow.read("api_key") if {
issuer = "github"
subject = "repo:owner/repo:ref:refs/heads/main"
}
# allow all variables starting with "github/"
allow.read(name) if {
startswith(name, "github/")
issuer = "github"
subject = "repo:owner/repo:ref:refs/heads/main"
}
# allow all variables to the configured issuers
allow.read(_)

allow.internal(name)

Determines if the variable name is internal. Internal variables are loaded during the policy evaluation for variable definitions, but are not intended to be returned in the response to the client.

policy.rego
# allow all variables starting with "internal/"
allow.internal(name) if {
startswith(name, "internal/")
# validate issuer, subject, claims
}

Policy Input

issuer

The identifier used in the configuration to identify the issuer that the token was issued by.

config.yaml
variables:
github_secret: "value"
issuers:
github:
issuer: https://token.actions.githubusercontent.com
policy.rego
allow.read("github_secret") if {
issuer = "github"
subject = "repo:owner/repo:ref:refs/heads/main"
claims.iss = "https://token.actions.githubusercontent.com"
}

subject

The sub claim of the token. This value is the same as claims.sub.

policy.rego
allow.read("github_secret") if {
issuer = "github"
subject = "repo:owner/repo:ref:refs/heads/main"
claims.sub = "repo:owner/repo:ref:refs/heads/main"
}

claims

An object with the validated token claims.

policy.rego
allow.read("github_secret") if {
issuer = "github"
claims.repository = "owner/repo"
claims.ref = "refs/heads/main"
}

params

An object with user-provided parameters.

policy.rego
allow.read("read_access") if {
issuer = "github"
claims.repository_owner = "owner"
params.scope = "read"
}
allow.read("write_access") if {
issuer = "github"
subject = "repo:owner/repo:ref:refs/heads/main"
params.scope = "write"
}

Variable Definitions

After the policy is evaluated to determine which variables are allowed, the policy is evaluated once more using the allowed variables value and can be used to derive new variables.

policy.rego
allow.read("defined_variable_name")
define.defined_variable_name.value = "variable value"
define.defined_variable_name.export = "ENV_VAR_NAME"

There are some considerations when using variable definitions:

  • Variable definitions are not allowed by default. Their name must be allowed using allow.read in order to be returned to the client.
  • The value of a variable definition must be a string.
  • The name of variable definitions cannot make use of variables and must be a literal string. For instance:
    • This is OK: define["defined_variable_name"].value = "foo"
    • This will error: define[variable_name].value = "foo" if variable_name == "defined_variable_name"

read(name)

This function reads the value of the variable name. This function is only intended to be used in variable definitions in the policy after the variable was allowed using allow.read or allow.internal.

policy.rego
allow.read("variable_name")
allow.internal("internal_variable")
define.variable_name.value = read("internal_variable")

If the variable was not allowed or could not be loaded, the rule evaluation will halt and a warning message will be logged.

Utilities

fetch

This function is a wrapper of the built-in http.send function. The request object expected is the same, but with some defaults:

  • The default HTTP method is GET.
  • The User-Agent header defaults to the ezoidc server version.
  • If the request is successful, the requested URL is logged at the debug level.
  • If the request fails, the requested URL is logged at the warn level.
policy.rego
fetch({
"method": "POST",
"url": "https://example.com/api",
"body": {
"json": true
},
"headers": {
"Authorization": "...",
},
}) = {
"status": "200 OK",
"status_code": 200,
"body": {...},
"raw_body": "...",
"headers": {...},
}

cloudflare_r2_temporary_credentials

Use the Cloudflare R2 API to generate temporary credentials. If successful, the API response object is returned. See Cloudflare’s documentation for more information.

policy.rego
cloudflare_r2_temporary_credentials({
# Cloudflare API token
"token": read("cloudflare_api_token"),
# Cloudflare account ID
"account_id": "023e105f4ecef8ad9ca31a8372d0c353",
# Parent access key ID (required)
"parent_access_key_id": read("access_key_id"),
# Cloudflare R2 bucket name (required)
"bucket": "bucket_name",
# Duration of the temporary credentials in seconds (required)
"ttl_seconds": 900,
# Permission for the temporary credentials (required)
"permission": "object-read-only",
# List of objects to generate temporary credentials for (optional)
"objects": ["object_name"],
# List of prefixes to generate temporary credentials for (optional)
"prefixes": ["example-prefix/"],
}) = {
"errors": [
{
"code": 1000,
"message": "string"
}
],
"messages": ["string"],
"result": {
"accessKeyId": "example-access-key-id",
"secretAccessKey": "example-secret-key",
"sessionToken": "example-session-token"
},
"success": true
}

Example

config.yaml
variables:
internal/cloudflare_account_id: <f028a08a135be5aa81d577eb0fece3a6>
internal/cloudflare_bucket: <bucket-name>
internal/cloudflare_token:
value: { env: CLOUDFLARE_TOKEN }
internal/parent_access_key_id:
value: { env: CLOUDFLARE_ACCESS_KEY_ID }
policy: |
allow.internal(name) if startswith(name, "internal/")
allow.read(name) if {
startswith(name, "r2/")
# validate issuer, subject, claims
}
# Export R2 credentials as variables for the AWS CLI
define["r2/aws_access_key_id"] = r2_variable("AWS_ACCESS_KEY_ID")
define["r2/aws_secret_access_key"] = r2_variable("AWS_SECRET_ACCESS_KEY")
define["r2/aws_session_token"] = r2_variable("AWS_SESSION_TOKEN")
define["r2/aws_endpoint_url"] = {
"value": sprintf("https://%s.r2.cloudflarestorage.com", [read("internal/cloudflare_account_id")]),
"export": "AWS_ENDPOINT_URL",
}
r2_variable(export) := {"value": value, "export": export} if {
response := cloudflare_r2_temporary_credentials({
"token": read("internal/cloudflare_token"),
"account_id": read("internal/cloudflare_account_id"),
"bucket": read("internal/cloudflare_bucket"),
"parentAccessKeyId": read("internal/parent_access_key_id"),
"permission": "object-read-only",
"ttlSeconds": 900,
})
map := {
"AWS_ACCESS_KEY_ID": "accessKeyId",
"AWS_SECRET_ACCESS_KEY": "secretAccessKey",
"AWS_SESSION_TOKEN": "sessionToken",
}
value := response.result[map[export]]
}

providers.aws.sign_req

This OPA built-in function is used to sign a request object using AWS Signature Version 4. See OPA’s documentation for more information.

policy.rego
request = {
"method": "GET",
"url": "https://bucket.s3.amazonaws.com/file.txt"
}
aws_config = {
"aws_access_key": read("aws_access_key"),
"aws_secret_access_key": read("aws_secret_access_key"),
"aws_service": "s3",
"aws_region": "us-east-1",
}
signed_request = providers.aws.sign_req(request, aws_config, time.now_ns())

github_app_jwt

This function generates a JWT for a GitHub App. See GitHub’s documentation for more information.

policy.rego
github_app_jwt({
# GitHub App private key in PEM format
"private_key": read("github_app_private_key")
# GitHub App ID
"app_id": 123,
}) = "jwt"

github_app_installation_token

This function generates a GitHub App installation token. See GitHub’s documentation for more information.

policy.rego
github_app_installation_token({
# GitHub App private key in PEM format
"private_key": read("github_app_private_key")
# GitHub App ID
"app_id": 123,
# GitHub App installation ID
"installation_id": 456,
# Body of the request to downscope the token (optional)
"body": {
"permissions": { "contents": "read" },
"repositories": ["org/repo"],
},
}) = "ghs_rwo6..."

Example

variables:
internal/github_app_private_key:
value: { env: GITHUB_APP_PRIVATE_KEY }
internal/github_app_id: <app-id>
internal/github_installation_id: <installation-id>
policy: |
allow.internal(name) if startswith(name, "internal/")
allow.read("github_token") if {
# validate issuer, subject, claims
}
define.github_token.export = "GH_TOKEN"
define.github_token.value = github_app_installation_token({
"private_key": read("internal/github_app_private_key"),
"app_id": read("internal/github_app_id"),
"installation_id": read("internal/github_installation_id"),
"body": {
"permissions": { "contents": "read" },
},
})

totp_verify

Validate a time-based one-time password (TOTP) code using HMAC-SHA1.

policy.rego
totp_verify({
# Shared secret (required)
"secret": read("totp_secret"),
# Code to validate (required)
"code": "123456",
# Time period in seconds
"period": 30,
# Number of time periods to allow for skew
"skew": 0,
# Current time in nanoseconds
"time": time.now_ns(),
}) = true

Example

policy.rego
allow.internal("totp_secret")
allow.read("privileged_token") if {
# validate issuer, subject, claims
}
define.privileged_token.value = "token" if {
totp_verify({
"secret": read("totp_secret"),
"code": params.code
})
}