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:
| Parameter | Required | Description | Example |
|---|---|---|---|
address | Yes | Redis server address (host:port) | db.example.com:6379 |
password | Yes | Admin password for ACL management | strong-password |
username | No | Admin username (default: default) | arcan_admin |
tls | No | Enable TLS (default: false) | true |
database | No | Redis database number (default: 0) | 0 |
max_ttl | No | Maximum lease duration (default: 24h) | 72h |
default_ttl | No | Default lease duration (default: 1h) | 4h |
max_connections | No | Max 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
| Role | ACL Rules | Use Case |
|---|---|---|
readonly | ~* +get +mget +hget +hgetall +lrange +smembers +zrange +scan +info | Cache reads, session lookups |
readwrite | ~* +get +set +del +mget +mset +hget +hset +hdel +hgetall +lpush +rpush +lrange +sadd +srem +zadd +zrem +expire +scan +info | Application backends |
admin | ~* +@all | Full 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:
| Role | Redis Command |
|---|---|
readonly | ACL SETUSER arcan_xxx on >password ~* +get +mget +hget +hgetall +lrange +smembers +sismember +zrange +ttl +type +exists +keys +scan +info |
readwrite | ACL 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 |
admin | ACL 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
Validatemethod refuses to delete users that don't have thearcan_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
readonlyrole uses explicit command allowlists rather than+@readto avoid granting access to potentially dangerous read commands likeDEBUG OBJECTorCLIENT 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 SAVEcommand should be configured in the core to persist ACL changes across Redis restarts, or the Redis server should useaclfileconfiguration.
Redis Version Compatibility
| Redis Version | ACL Support | Notes |
|---|---|---|
| < 6.0 | No | Not supported -- single-password auth only |
| 6.0 - 6.2 | Yes | Basic ACL support |
| 7.0+ | Yes | Enhanced 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.