Skip to main content

RabbitMQ -- Dynamic User Credentials

Creates temporary RabbitMQ users with specific vhost permissions. Each generate call creates a dedicated user with a random password and scoped permissions. Revocation deletes the user, immediately terminating all connections.

Capabilities

CapabilityDescription
secret_generationCreates RabbitMQ users with vhost permissions
secret_validationRevokes credentials by deleting the user

Configuration

ParameterRequiredDescriptionExample
management_urlRequiredRabbitMQ Management API URL (HTTP)http://rabbitmq.example.com:15672
admin_usernameRequiredAdmin user for the Management APIadmin
admin_passwordRequiredAdmin passwords3cret
default_vhostOptionalDefault vhost for permissions. Defaults to /production
password_lengthOptionalLength of generated passwords. Defaults to 3248

The admin user must have the administrator tag in RabbitMQ to create/delete users and set permissions.

Roles

RoleConfigureWriteReadUse Case
consumer^$^$.*Read-only -- consume from any queue
producer^$.*^$Write-only -- publish to any exchange
admin.*.*.*Full access -- create queues, exchanges, bind, publish, consume

Permission patterns are RabbitMQ regex patterns applied to resource names within the vhost:

  • configure: create/delete queues and exchanges
  • write: publish to exchanges, bind queues
  • read: consume from queues, bind to exchanges

Operations

generate

Creates a new RabbitMQ user with a random password and sets vhost permissions.

Request:

{
"method": "generate",
"params": {
"role": "consumer",
"vhost": "production"
}
}

What happens internally:

  1. Generates a unique username: arcan-<random-8-chars>
  2. Generates a random password (32 chars by default)
  3. Creates the user via PUT /api/users/{username} with a password hash
  4. Sets vhost permissions via PUT /api/permissions/{vhost}/{username} with role-specific patterns
  5. Returns credentials and connection details

Response:

{
"data": {
"username": "arcan-a1b2c3d4",
"password": "xK9mP2qR7vN4wL8jB3hF6tY1cA5eD0gS",
"vhost": "production",
"management_url": "http://rabbitmq.example.com:15672",
"amqp_url": "amqp://arcan-a1b2c3d4:[email protected]:5672/production",
"permissions": {
"configure": "^$",
"write": "^$",
"read": ".*"
}
}
}

validate (revoke)

Deletes the RabbitMQ user, immediately closing all connections.

Request:

{
"method": "validate",
"params": {
"username": "arcan-a1b2c3d4"
}
}

What happens internally:

  1. Deletes the user via DELETE /api/users/{username}
  2. RabbitMQ closes all connections belonging to this user

Response:

{
"data": {
"valid": false,
"message": "user arcan-a1b2c3d4 deleted"
}
}

Complete Go Plugin Source

package main

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"os"
"strings"
"time"

"getarcan.dev/arcan/sdk"
)

type RabbitMQEngine struct {
mgmtURL string
adminUser string
adminPass string
vhost string
passLen int
}

type generateParams struct {
Role string `json:"role"`
VHost string `json:"vhost"`
}

type revokeParams struct {
Username string `json:"username"`
}

type rolePerms struct {
Configure string
Write string
Read string
}

var roles = map[string]rolePerms{
"consumer": {Configure: "^$", Write: "^$", Read: ".*"},
"producer": {Configure: "^$", Write: ".*", Read: "^$"},
"admin": {Configure: ".*", Write: ".*", Read: ".*"},
}

func (e *RabbitMQEngine) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "rabbitmq",
Version: "0.1.0",
DisplayName: "RabbitMQ",
Description: "Dynamic user credentials with vhost permissions",
Capabilities: []string{"secret_generation", "secret_validation"},
}
}

func (e *RabbitMQEngine) Ping() error {
req, err := http.NewRequest("GET", e.mgmtURL+"/api/overview", nil)
if err != nil {
return err
}
req.SetBasicAuth(e.adminUser, e.adminPass)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("cannot reach RabbitMQ management API: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return fmt.Errorf("RabbitMQ management API returned %d", resp.StatusCode)
}
return nil
}

func (e *RabbitMQEngine) Generate(params json.RawMessage) (*sdk.SecretResult, error) {
var p generateParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid params: %w", err)
}

perms, ok := roles[p.Role]
if !ok {
return nil, fmt.Errorf("unknown role %q -- valid roles: consumer, producer, admin", p.Role)
}

vhost := p.VHost
if vhost == "" {
vhost = e.vhost
}

username := "arcan-" + randomHex(4)
password := randomPassword(e.passLen)

// 1. Create user
userBody := fmt.Sprintf(`{"password":%q,"tags":""}`, password)
if err := e.rabbitAPI("PUT", "/api/users/"+url.PathEscape(username), userBody); err != nil {
return nil, fmt.Errorf("creating user: %w", err)
}

// 2. Set permissions
permBody := fmt.Sprintf(`{"configure":%q,"write":%q,"read":%q}`, perms.Configure, perms.Write, perms.Read)
permPath := fmt.Sprintf("/api/permissions/%s/%s", url.PathEscape(vhost), url.PathEscape(username))
if err := e.rabbitAPI("PUT", permPath, permBody); err != nil {
// Clean up user on permission failure
e.rabbitAPI("DELETE", "/api/users/"+url.PathEscape(username), "")
return nil, fmt.Errorf("setting permissions: %w", err)
}

// Build AMQP URL
mgmtParsed, _ := url.Parse(e.mgmtURL)
amqpHost := mgmtParsed.Hostname()
amqpURL := fmt.Sprintf("amqp://%s:%s@%s:5672/%s",
url.PathEscape(username), url.PathEscape(password),
amqpHost, url.PathEscape(vhost))

return &sdk.SecretResult{
Data: map[string]any{
"username": username,
"password": password,
"vhost": vhost,
"management_url": e.mgmtURL,
"amqp_url": amqpURL,
"permissions": map[string]string{
"configure": perms.Configure,
"write": perms.Write,
"read": perms.Read,
},
},
}, nil
}

func (e *RabbitMQEngine) Validate(params json.RawMessage) (*sdk.ValidationResult, error) {
var p revokeParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid params: %w", err)
}

if p.Username == "" {
return nil, fmt.Errorf("username is required")
}

if err := e.rabbitAPI("DELETE", "/api/users/"+url.PathEscape(p.Username), ""); err != nil {
return nil, fmt.Errorf("deleting user: %w", err)
}

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

func (e *RabbitMQEngine) rabbitAPI(method, path, body string) error {
var bodyReader io.Reader
if body != "" {
bodyReader = strings.NewReader(body)
}

req, err := http.NewRequest(method, e.mgmtURL+path, bodyReader)
if err != nil {
return err
}
req.SetBasicAuth(e.adminUser, e.adminPass)
req.Header.Set("Content-Type", "application/json")

resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("RabbitMQ API %s %s returned %d: %s", method, path, resp.StatusCode, string(respBody))
}
return nil
}

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

func randomPassword(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
b[i] = charset[n.Int64()]
}
return string(b)
}

func main() {
mgmtURL := os.Getenv("ARCAN_RABBITMQ_URL")
adminUser := os.Getenv("ARCAN_RABBITMQ_USER")
adminPass := os.Getenv("ARCAN_RABBITMQ_PASS")
vhost := os.Getenv("ARCAN_RABBITMQ_VHOST")

if mgmtURL == "" || adminUser == "" || adminPass == "" {
fmt.Fprintf(os.Stderr, "set ARCAN_RABBITMQ_URL, ARCAN_RABBITMQ_USER, ARCAN_RABBITMQ_PASS\n")
os.Exit(1)
}

mgmtURL = strings.TrimRight(mgmtURL, "/")
if vhost == "" {
vhost = "/"
}

eng := &RabbitMQEngine{
mgmtURL: mgmtURL,
adminUser: adminUser,
adminPass: adminPass,
vhost: vhost,
passLen: 32,
}
sdk.Serve(eng)
}

Build & Install

mkdir -p engines/rabbitmq && cd engines/rabbitmq
go mod init getarcan.dev/engines/rabbitmq
go get getarcan.dev/arcan/sdk

# Build
go build -o arcan-engine-rabbitmq .

# Install
cp arcan-engine-rabbitmq ~/.arcan/plugins/

Usage

# Set connection
export ARCAN_RABBITMQ_URL="http://rabbitmq.example.com:15672"
export ARCAN_RABBITMQ_USER="admin"
export ARCAN_RABBITMQ_PASS="s3cret"

# Ping
echo '{"method":"ping","params":{}}' | ./arcan-engine-rabbitmq
# => {"data":{"status":"healthy"}}

# Generate consumer credentials
echo '{"method":"generate","params":{"role":"consumer","vhost":"production"}}' | ./arcan-engine-rabbitmq
# => {"data":{"username":"arcan-a1b2c3d4","password":"xK9m...","amqp_url":"amqp://...",...}}

# Use the credentials
# Python: pika.PlainCredentials("arcan-a1b2c3d4", "xK9m...")
# Node: amqplib.connect("amqp://arcan-a1b2c3d4:xK9m...@rabbitmq:5672/production")

# Revoke
echo '{"method":"validate","params":{"username":"arcan-a1b2c3d4"}}' | ./arcan-engine-rabbitmq
# => {"data":{"valid":false,"message":"user arcan-a1b2c3d4 deleted"}}

Security Notes

  • The admin credentials are the root of trust. Store them in Arcan's own KV engine or inject via environment variables -- never hardcode.
  • RabbitMQ Management API uses HTTP Basic Auth. Always run behind TLS (use https:// for the management URL in production).
  • Deleting a user via the Management API immediately closes all AMQP connections for that user.
  • Generated passwords use crypto/rand for cryptographic randomness.
  • All created usernames are prefixed with arcan- for easy identification. Consider adding a periodic cleanup job to remove stale users.
  • The tags field is set to empty string (no management access). Dynamic users should never have management UI access.
  • Vhost must exist before the engine can set permissions on it. The engine does not create vhosts.
  • Permission patterns use RabbitMQ's regex syntax. The default roles use broad patterns -- customize for specific queue/exchange names in production.