Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/content/docs/accounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,31 @@ accounting {
```

Multiple rules can co-exist. DRL evaluates rules in order and applies the **first matching** rule to an entity.

## Overriding rules via environment variables

Individual rules can be injected or overridden without a config file using `DRL_RULE_<rule-name>_JSON`.
The variable value is a JSON object with the same fields as the KDL rule block.

```bash
# Override a single rule limit for staging
DRL_RULE_payments_api_JSON='{"path-prefix":"/api/v1/payments","limit":50,"per":"minute"}'

# Add a rule that does not exist in the base KDL config
DRL_RULE_health_JSON='{"path-prefix":"/health","limit":10,"per":"second"}'
```

**Merge semantics:** env-var rules are applied after the KDL file is parsed. A rule name present in an
env var overwrites the corresponding KDL rule; all other KDL rules remain unchanged. This makes it
straightforward to ship a shared base config and tune per-environment limits without duplicating the
full rule set.

**Field reference (JSON keys):**

| JSON key | Required | Description |
|----------|----------|-------------|
| `path-prefix` | Yes | URI path prefix to match |
| `headers` | No | Array of header names to include in the entity key |
| `redactions` | No | Object mapping header name → redaction regex |
| `limit` | Yes | Request count threshold |
| `per` | Yes | Window unit: `"second"` or `"minute"` |
70 changes: 70 additions & 0 deletions docs/content/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,26 @@ accounting {
DRL evaluates rules in definition order and applies the **first matching rule** to an entity. Entities that
match no rule are passed through without accounting.

#### Environment variable override

Individual rules can be injected or overridden without a config file via `DRL_RULE_<rule-name>_JSON`.
The value is a JSON object matching the rule fields above.

```bash
DRL_RULE_payments_api_JSON='{"path-prefix":"/api/v1/payments","headers":["X-API-Key","X-Tenant-ID"],"redactions":{"X-API-Key":"^(.{0,3}).*$"},"limit":500,"per":"minute"}'
DRL_RULE_users_api_JSON='{"path-prefix":"/api/v1/users","limit":2000,"per":"minute"}'
```

**Merge semantics:** env-var rules are merged on top of any KDL-defined rules. A rule whose name appears
in an env var overwrites the KDL rule with the same name; rules not mentioned in env vars are kept
as-is. This lets you ship a base rule set in a KDL file and patch specific limits per environment
(e.g. lower limits in staging).

> **Note:** The `<rule-name>` portion of the env var key is used verbatim as the map key. It must
> exactly match the name used in the KDL `rules {}` block when you intend to override an existing rule.
> Because POSIX shell variable names cannot contain hyphens, prefer underscores in rule names
> (e.g. `payments_api` rather than `payments-api`) when env-var overrides are required.

---

### Header Redactions
Expand Down Expand Up @@ -396,6 +416,56 @@ full architecture and OIDC reference.
| `tls.cert` | `DRL_EMBEDDED_PROXY_TLS_CERT` | — | Base64-encoded PEM certificate |
| `tls.key` | `DRL_EMBEDDED_PROXY_TLS_KEY` | — | Base64-encoded PEM private key |

#### Host and route override

The `host` / `routes` tree is too deeply nested for flat env vars. The entire hosts array can be
replaced via `DRL_EMBEDDED_PROXY_HOSTS_JSON` (full replace, not merge):

```bash
DRL_EMBEDDED_PROXY_HOSTS_JSON='[
{
"hostname": "api.example.com",
"oidc": {
"issuer": "https://auth.example.com/realms/myapp",
"client-id": "drl-gateway",
"audience": "https://api.example.com"
},
"routes": {
"routes": [
{
"prefix": "/v1",
"upstream": "http://backend:8080",
"require-auth": true,
"scopes": ["read"]
},
{
"prefix": "/health",
"upstream": "http://backend:8080",
"require-auth": false
}
]
}
}
]'
```

JSON field names follow the `json:"..."` struct tags (same as the keys reported by the internal API).
The `routes` wrapper is a nested object — see the example above for the correct shape.

**Terraform** users can produce the value with `jsonencode(...)`:

```hcl
environment = [
{
name = "DRL_EMBEDDED_PROXY_HOSTS_JSON"
value = jsonencode([{
hostname = "api.example.com"
routes = { routes = [{ prefix = "/", upstream = "http://backend:8080", "require-auth" = false }] }
}])
}
]
```

#### `embedded-proxy.host.<hostname>.routes.route`

Each `route` node inside a `host` block maps a URI prefix to an upstream service.
Expand Down
61 changes: 61 additions & 0 deletions docs/content/docs/embedded-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,67 @@ them. Token issuance remains the responsibility of your IdP.

---

## Environment variable override

The `host` / `routes` tree is too deeply nested for flat env vars. Set
`DRL_EMBEDDED_PROXY_HOSTS_JSON` to replace the entire hosts array at runtime — useful for
container environments (Docker Compose, Kubernetes, ECS) where mounting a config file is
inconvenient.

```bash
DRL_EMBEDDED_PROXY_HOSTS_JSON='[
{
"hostname": "api.example.com",
"oidc": {
"issuer": "https://auth.example.com/realms/myapp",
"client-id": "drl-gateway",
"audience": "https://api.example.com"
},
"routes": {
"routes": [
{
"prefix": "/v1",
"upstream": "http://backend:8080",
"require-auth": true,
"scopes": ["read"]
},
{
"prefix": "/health",
"upstream": "http://backend:8080",
"require-auth": false
}
]
}
}
]'
```

> **Shape note:** `routes` is a wrapper object — the inner array is at `routes.routes`. This mirrors
> the internal JSON representation returned by the config API.

**Terraform:**

```hcl
environment = [
{
name = "DRL_EMBEDDED_PROXY_HOSTS_JSON"
value = jsonencode([{
hostname = "api.example.com"
routes = { routes = [
{ prefix = "/v1", upstream = "http://backend:8080", "require-auth" = true, scopes = ["read"] },
{ prefix = "/health", upstream = "http://backend:8080", "require-auth" = false }
]}
}])
}
]
```

Scalar settings (`enabled`, `listen`, `tls.*`) continue to be overridable via their individual
`DRL_EMBEDDED_PROXY_*` env vars. `DRL_EMBEDDED_PROXY_HOSTS_JSON` only controls the hosts/routes
tree.

---

## Basic Configuration

```kdl
Expand Down
55 changes: 55 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"embed"
"encoding/json"
"fmt"
"os"
"regexp"
Expand Down Expand Up @@ -286,6 +287,14 @@ func Load(configPath string) (*Config, error) {
// (DRL_MEMBERSHIP_PRIMARY_KEY + DRL_MEMBERSHIP_SECONDARY_KEYS)
cfg.applyEncryptionKeyEnvOverrides()

// Apply JSON blob overrides for complex structures not representable as flat env vars
if err = cfg.applyRuleJSONOverrides(); err != nil {
return nil, err
}
if err = cfg.applyProxyHostsJSONOverride(); err != nil {
return nil, err
}

// Validate the final configuration
if err = cfg.Validate(); err != nil {
return nil, fmt.Errorf("configuration validation failed: %w", err)
Expand All @@ -310,6 +319,52 @@ func (c *Config) loadFromEnvironment() error {
return nil
}

// ruleJSONEnvRe matches DRL_RULE_<name>_JSON env var names and captures the rule name.
// The name may contain underscores; the greedy .+ stops at the trailing _JSON suffix.
var ruleJSONEnvRe = regexp.MustCompile(`^DRL_RULE_(.+)_JSON$`)

// applyRuleJSONOverrides scans the environment for DRL_RULE_<name>_JSON variables and
// merges each decoded AccountingRule into c.Accounting.Rules, overwriting any
// KDL-configured rule with the same name. Rules not mentioned in env vars are preserved.
func (c *Config) applyRuleJSONOverrides() error {
for _, kv := range os.Environ() {
idx := strings.IndexByte(kv, '=')
if idx < 0 {
continue
}
key, val := kv[:idx], kv[idx+1:]
m := ruleJSONEnvRe.FindStringSubmatch(key)
if m == nil {
continue
}
name := m[1]
var rule AccountingRule
if err := json.Unmarshal([]byte(val), &rule); err != nil {
return fmt.Errorf("%s is not valid JSON: %w", key, err)
}
if c.Accounting.Rules == nil {
c.Accounting.Rules = make(map[string]AccountingRule)
}
c.Accounting.Rules[name] = rule
}
return nil
}

// applyProxyHostsJSONOverride applies DRL_EMBEDDED_PROXY_HOSTS_JSON, fully replacing
// the hosts slice when the variable is set.
func (c *Config) applyProxyHostsJSONOverride() error {
raw := os.Getenv("DRL_EMBEDDED_PROXY_HOSTS_JSON")
if raw == "" {
return nil
}
var hosts []ProxyHostConfig
if err := json.Unmarshal([]byte(raw), &hosts); err != nil {
return fmt.Errorf("DRL_EMBEDDED_PROXY_HOSTS_JSON is not valid JSON: %w", err)
}
c.EmbeddedProxy.Hosts = hosts
return nil
}

// applyEncryptionKeyEnvOverrides checks DRL_MEMBERSHIP_PRIMARY_KEY and
// DRL_MEMBERSHIP_SECONDARY_KEYS environment variables. If the primary key
// env var is set, it overrides SecretKeys entirely (env > KDL precedence).
Expand Down
Loading