Skip to main content

Redis — Dynamic Credentials Engine

Generates temporary Redis ACL users with scoped permissions and automatic revocation.

Overview

The Redis engine creates short-lived Redis users using the ACL system (Redis 6+). When an application requests credentials, the engine issues ACL SETUSER commands with specific permission rules (key patterns, allowed commands). When the lease expires or is revoked, the engine deletes the user with ACL DELUSER.

The plugin communicates with Arcan core via JSON over stdin/stdout. It does NOT connect to Redis directly -- the core holds the admin credentials and executes commands on the plugin's behalf via ctx.HTTP host functions (or a Redis-specific command execution path in the core adapter).

External system: Redis 6.0+ (requires ACL support)

Capabilities

  • engine:dynamic_credentials -- create and revoke temporary ACL users

Engine Descriptor

{
"name": "redis",
"version": "0.1.0",
"display_name": "Redis",
"description": "Dynamic credentials for Redis using ACL system",
"sdk_version": 1,
"min_core_version": "0.1.0",
"capabilities": [
"host:http",
"host:store:read",
"host:store:write",
"host:audit",
"engine:dynamic_credentials"
],
"tier": "official",
"config_schema": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Redis server address",
"example": "db.example.com:6379"
},
"username": {
"type": "string",
"description": "Admin username for ACL management",
"default": "default"
},
"password": {
"type": "string",
"description": "Admin password",
"format": "password"
},
"tls": {
"type": "boolean",
"description": "Enable TLS connection",
"default": false
},
"database": {
"type": "integer",
"description": "Redis database number",
"default": 0
},
"max_ttl": {
"type": "string",
"description": "Maximum lease TTL (Go duration)",
"default": "24h"
},
"default_ttl": {
"type": "string",
"description": "Default lease TTL if not specified",
"default": "1h"
},
"max_connections": {
"type": "integer",
"description": "Max concurrent leased credentials",
"default": 50
}
},
"required": ["address", "password"]
},
"default_roles": [
{ "name": "readonly", "config": { "acl_rules": "~* +get +mget +hget +hgetall +lrange +smembers +sismember +zrange +ttl +type +exists +keys +scan +info" } },
{ "name": "readwrite", "config": { "acl_rules": "~* +get +set +del +mget +mset +hget +hset +hdel +hgetall +lpush +rpush +lpop +rpop +lrange +sadd +srem +smembers +zadd +zrem +zrange +expire +ttl +type +exists +keys +scan +info" } },
{ "name": "admin", "config": { "acl_rules": "~* +@all" } }
]
}

Configuration

What the admin provides during arcan plugin setup redis:

ParameterRequiredDescriptionExample
addressYesRedis server address (host:port)db.example.com:6379
passwordYesAdmin password for ACL managementstrong-password
usernameNoAdmin username (default: default)arcan_admin
tlsNoEnable TLS (default: false)true
databaseNoRedis database number (default: 0)0
max_ttlNoMaximum lease duration (default: 24h)72h
default_ttlNoDefault lease duration (default: 1h)4h
max_connectionsNoMax concurrent leased credentials (default: 50)100

Bootstrap

During setup, the core connects to Redis and verifies connectivity with PING. The admin should configure Redis with ACL support enabled and an admin user capable of managing other users:

# redis.conf (or via CLI)
# Enable ACL file persistence
aclfile /etc/redis/users.acl

# Create admin user (via redis-cli)
ACL SETUSER arcan_admin on >strong-password ~* +@all

Alternatively, with Redis 6+ default setup, the default user with a password can manage ACLs.

Roles

RoleACL RulesUse Case
readonly~* +get +mget +hget +hgetall +lrange +smembers +zrange +scan +infoCache reads, session lookups
readwrite~* +get +set +del +mget +mset +hget +hset +hdel +hgetall +lpush +rpush +lrange +sadd +srem +zadd +zrem +expire +scan +infoApplication backends
admin~* +@allFull access, administration

ACL Rule Breakdown

Redis ACL rules control three dimensions:

  • Key patterns: ~* (all keys), ~cache:* (only cache keys), ~session:* (only session keys)
  • Commands: +get (allow GET), -flushall (deny FLUSHALL), +@read (allow all read commands)
  • Channels: &* (all pub/sub channels) -- not used by default roles

Operations

describe

Returns the engine descriptor.

Request:

{"method": "describe"}

Response:

{
"data": {
"name": "redis",
"version": "0.1.0",
"display_name": "Redis",
"description": "Dynamic credentials for Redis using ACL system",
"capabilities": ["dynamic_credentials"]
}
}

ping

Verifies connectivity to the Redis instance.

Request:

{"method": "ping"}

Redis command executed:

PING

Response (healthy):

{"data": {"status": "healthy"}}

Response (unhealthy):

{"error": "ping failed: connection refused"}

generate (Create Credentials)

Creates a temporary Redis ACL user with scoped permissions.

Request:

{"method": "generate", "params": {"role": "readonly", "ttl": "1h"}}

Redis command executed:

ACL SETUSER arcan_7f3a9b2c on >xK9mP2qR4vL7nW8y ~* +get +mget +hget +hgetall +lrange +smembers +sismember +zrange +ttl +type +exists +keys +scan +info

Response:

{
"data": {
"username": "arcan_7f3a9b2c",
"password": "xK9mP2qR4vL7nW8y",
"expires_at": "2026-04-03T12:00:00Z",
"address": "db.example.com:6379",
"database": 0
}
}

ACL commands by role:

RoleRedis Command
readonlyACL SETUSER arcan_xxx on >password ~* +get +mget +hget +hgetall +lrange +smembers +sismember +zrange +ttl +type +exists +keys +scan +info
readwriteACL SETUSER arcan_xxx on >password ~* +get +set +del +mget +mset +hget +hset +hdel +hgetall +lpush +rpush +lpop +rpop +lrange +sadd +srem +smembers +zadd +zrem +zrange +expire +ttl +type +exists +keys +scan +info
adminACL SETUSER arcan_xxx on >password ~* +@all

validate (Revoke Credentials)

Revokes a previously issued credential by deleting the Redis ACL user. The "validate" method is used for revocation in the current SDK protocol -- it validates that the credential was successfully revoked.

Request:

{"method": "validate", "params": {"username": "arcan_7f3a9b2c"}}

Redis command executed:

ACL DELUSER arcan_7f3a9b2c

Response:

{
"data": {
"valid": true,
"message": "credentials revoked: user arcan_7f3a9b2c deleted"
}
}

Complete Plugin Source

// Arcan Redis Dynamic Credentials Engine
//
// Build: go build -o arcan-engine-redis .
// Install: cp arcan-engine-redis ~/.arcan/data/plugins/
package main

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"

"getarcan.dev/arcan/sdk"
)

type RedisEngine struct{}

func (e *RedisEngine) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "redis",
Version: "0.1.0",
DisplayName: "Redis",
Description: "Dynamic credentials for Redis using ACL system",
Capabilities: []string{
"dynamic_credentials",
},
}
}

func (e *RedisEngine) Ping() error {
return nil
}

func (e *RedisEngine) Generate(params json.RawMessage) (*sdk.SecretResult, error) {
var p struct {
Role string `json:"role"`
TTL string `json:"ttl"`
}
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("parsing params: %w", err)
}
if p.Role == "" {
p.Role = "readonly"
}
if p.TTL == "" {
p.TTL = "1h"
}

ttl, err := time.ParseDuration(p.TTL)
if err != nil {
return nil, fmt.Errorf("invalid ttl %q: %w", p.TTL, err)
}

aclRules, err := aclRulesForRole(p.Role)
if err != nil {
return nil, err
}

username := generateUsername()
password := generatePassword(24)
expiresAt := time.Now().UTC().Add(ttl).Format(time.RFC3339)

// Build the ACL command for the core to execute.
aclCommand := fmt.Sprintf("ACL SETUSER %s on >%s %s", username, password, aclRules)

return &sdk.SecretResult{
Data: map[string]any{
"username": username,
"password": password,
"expires_at": expiresAt,
"redis_command": aclCommand,
},
}, nil
}

func (e *RedisEngine) Validate(params json.RawMessage) (*sdk.ValidationResult, error) {
var p struct {
Username string `json:"username"`
}
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("parsing params: %w", err)
}
if p.Username == "" {
return nil, fmt.Errorf("username is required")
}
if !strings.HasPrefix(p.Username, "arcan_") {
return nil, fmt.Errorf("refusing to delete non-arcan user %q", p.Username)
}

return &sdk.ValidationResult{
Valid: true,
Message: fmt.Sprintf("credentials revoked: user %s deleted", p.Username),
}, nil
}

func aclRulesForRole(role string) (string, error) {
switch role {
case "readonly":
return "~* +get +mget +hget +hgetall +lrange +smembers +sismember +zrange +ttl +type +exists +keys +scan +info", nil
case "readwrite":
return "~* +get +set +del +mget +mset +hget +hset +hdel +hgetall +lpush +rpush +lpop +rpop +lrange +sadd +srem +smembers +zadd +zrem +zrange +expire +ttl +type +exists +keys +scan +info", nil
case "admin":
return "~* +@all", nil
default:
return "", fmt.Errorf("unknown role %q — use readonly, readwrite, or admin", role)
}
}

func generateUsername() string {
b := make([]byte, 4)
rand.Read(b)
return "arcan_" + hex.EncodeToString(b)
}

func generatePassword(length int) string {
b := make([]byte, length)
rand.Read(b)
return hex.EncodeToString(b)[:length]
}

func main() {
sdk.Serve(&RedisEngine{})
}

Build & Install

# Build native binary
cd engines/redis/
go build -o arcan-engine-redis .

# Build WASM (future — requires wazero runtime in core)
GOOS=wasip1 GOARCH=wasm go build -o redis.wasm .

# Install to plugin directory
cp arcan-engine-redis ~/.arcan/data/plugins/

# Verify discovery
arcan server &
# Check logs for: "plugin loaded" name=redis version=0.1.0

go.mod

module getarcan.dev/engines/redis

go 1.26

require getarcan.dev/arcan v0.0.0

Usage

# Setup the engine (interactive wizard from config_schema)
arcan plugin setup redis

# Generate readonly credentials (1 hour TTL)
curl -s -X POST https://localhost:8081/api/v1/realms/default/engines/redis/generate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"role": "readonly", "ttl": "1h"}' | jq .

# Response:
# {
# "username": "arcan_7f3a9b2c",
# "password": "xK9mP2qR4vL7nW8y",
# "expires_at": "2026-04-03T12:00:00Z",
# "address": "db.example.com:6379",
# "database": 0
# }

# Use the credentials
redis-cli -h db.example.com -p 6379 --user arcan_7f3a9b2c --pass xK9mP2qR4vL7nW8y

# Revoke credentials early
curl -s -X POST https://localhost:8081/api/v1/realms/default/engines/redis/validate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"username": "arcan_7f3a9b2c"}' | jq .

# Generate admin credentials (30 minute TTL)
curl -s -X POST https://localhost:8081/api/v1/realms/default/engines/redis/generate \
-H "Authorization: Bearer arc_xxx" \
-H "Content-Type: application/json" \
-d '{"role": "admin", "ttl": "30m"}' | jq .

How the Core Executes Redis Commands

Redis uses its own protocol (RESP), not SQL. The plugin returns the ACL command string in the redis_command field. The core's plugin adapter translates this into Redis protocol calls using the Go Redis client:

Plugin returns:
{"redis_command": "ACL SETUSER arcan_xxx on >password ~* +get +set"}

Core adapter does:
client.Do(ctx, "ACL", "SETUSER", "arcan_xxx", "on", ">password", "~*", "+get", "+set")

For revocation, the core runs:

  client.Do(ctx, "ACL", "DELUSER", "arcan_xxx")

This keeps the plugin as a pure stdin/stdout binary with zero external dependencies, while the core handles all Redis connectivity.

Security Notes

  • Usernames are prefixed with arcan_ to prevent accidental deletion of non-managed users.
  • The Validate method refuses to delete users that don't have the arcan_ prefix.
  • Passwords are crypto-random hex strings (24 characters = 96 bits of entropy).
  • Redis does not have a native credential expiry mechanism. Expiry is enforced by the Arcan lease reaper, which calls validate (revoke) when the TTL expires.
  • The readonly role uses explicit command allowlists rather than +@read to avoid granting access to potentially dangerous read commands like DEBUG OBJECT or CLIENT LIST.
  • The admin credentials are never exposed to the plugin -- the core holds and uses them directly.
  • Redis ACL requires Redis 6.0+. Older versions do not support per-user authentication.
  • The ACL SAVE command should be configured in the core to persist ACL changes across Redis restarts, or the Redis server should use aclfile configuration.

Redis Version Compatibility

Redis VersionACL SupportNotes
< 6.0NoNot supported -- single-password auth only
6.0 - 6.2YesBasic ACL support
7.0+YesEnhanced ACL with selectors, key permissions per command

The plugin targets Redis 6.0+ ACL syntax for maximum compatibility. Redis 7.0 selector syntax can be added as an optional enhancement in a future version.