Authentication and Authorization¶
Paladin supports optional RPC authorizers for securing JSON-RPC endpoints.
Overview¶
RPC authorizers allow you to secure Paladin's RPC endpoints. They support both
- authentication: using the HTTP headers to verify who is making the request
- authorization: using the JSON-RPC method and parameters to determine if the authenticated principal is permitted to perform a specific RPC operation
Plugin Architecture¶
RPC authorizers are implemented as Paladin plugins, which means they:
- Run as separate processes communicating with the Paladin core via gRPC, providing isolation and security
- Follow the standard Paladin plugin architecture and lifecycle patterns
- Can be developed using the plugin toolkit (Go, Java, or other languages with gRPC support)
- Are loaded dynamically and can be configured per node without modifying core code
This plugin architecture provides several benefits:
- Isolation: Authorization plugin failures don't crash the core Paladin system
- Security: Authorization logic runs in separate processes, limiting potential attack surface
- Extensibility: New authorization schemes can be added by implementing custom plugins
- Flexibility: Different nodes can use different authorization plugins based on their requirements
The reference basicauth plugin demonstrates the standard plugin implementation pattern, which can be used as a template for creating custom authorization plugins.
Multiple Authorizers¶
You can configure multiple authorizers that are executed sequentially in a chain. All authorizers in the chain must succeed for a request to proceed:
When multiple authorizers are configured:
- Authentication phase: Each authorizer's
Authenticate()method is called in sequence. If any authorizer fails authentication, the request is rejected immediately. - Authorization phase: Each authorizer's
Authorize()method is called in sequence with the corresponding authentication result. If any authorizer denies authorization, the request is rejected immediately. - All authorizers must succeed for the request to proceed to the RPC method handler.
Request Lifecycle¶
HTTP Requests¶
For HTTP requests, both authentication and authorization happen for each request:
- Request arrives → HTTP headers are extracted
- Authentication phase: For each authorizer in the chain:
Authenticate(headers)is called- If authentication fails → Return HTTP 401 Unauthorized (native transport response, no JSON body) (stop processing)
- Store the authentication result
- Authorization phase: For each authorizer in the chain:
Authorize(authenticationResult, method, payload)is called- If authorization fails → Return JSON-RPC error response (stop processing)
- All authorizers succeed → Proceed to RPC method handler
WebSocket Requests¶
For WebSocket connections, authentication happens once during the upgrade, and authorization happens for each RPC message:
Connection Upgrade Phase:
- Upgrade request arrives → HTTP headers are extracted from the WebSocket upgrade request
- Authentication phase: For each authorizer in the chain:
Authenticate(headers)is called before WebSocket upgrade- If authentication fails → Return HTTP 401 Unauthorized (native transport response, no JSON body), abort upgrade (stop processing)
- Store the authentication result
- All authorizers succeed → WebSocket upgrade proceeds (HTTP 101)
- Store authentication results in the WebSocket connection context
Subsequent RPC Messages:
- RPC message arrives → Retrieve stored authentication results from connection
- Authorization phase: For each authorizer in the chain:
Authorize(authenticationResult[i], method, payload)is called- If authorization fails → Return JSON-RPC error response (stop processing)
- All authorizers succeed → Proceed to RPC method handler
Note: WebSocket connections authenticate once during upgrade, but authorize each RPC message. This allows long-lived connections while still enforcing authorization checks on every operation. Authentication failures return native HTTP 401 responses, while authorization failures return JSON-RPC error responses.
Basic Auth Plugin¶
The reference basic auth plugin implements HTTP Basic Authentication, where credentials are sent in the Authorization header of each HTTP request.
Creating a Password File¶
Paladin uses standard bcrypt format compatible with htpasswd:
# Create a new password file
htpasswd -cB credentials.txt alice
# Add additional users
htpasswd -B credentials.txt bob
Expected format: Standard htpasswd bcrypt output
Configuration¶
Add to your Paladin configuration:
{
"rpcAuthorizers": {
"basicauth": {
"plugin": {
"library": "basicauth.so",
"type": "rpc_auth"
},
"config": "{\"credentialsFile\": \"/path/to/users.txt\"}"
}
},
"rpcServer": {
"authorizers": ["basicauth"]
}
}
Key Fields:
- rpcAuthorizers: Map of named authorization plugin configurations
- rpcServer.authorizers: Ordered array of authorizer plugin names to use
Configuration via Helm Charts (Customnet)¶
When deploying Paladin nodes using Helm charts in customnet mode, RPC authorization can be configured directly through the values file.
Important Deployment Workflow
When enabling RPC authorization on a customnet deployment, the operator needs to submit transactions to deploy smart contracts and register nodes. These operations will fail if RPC authentication is enabled from the start, as the operator cannot authenticate to the RPC endpoints.
Required Deployment Sequence:
-
Deploy without authentication: Initially deploy your Paladin nodes without RPC authorization configured in the values file.
-
Wait for deployment completion: Verify that all smart contract deployments, transaction invocations, and node registrations have completed successfully:
# Check smart contract deployments (all should show "Success" in STATUS column) kubectl get scd -n <your-namespace> # Check transaction invokes (all should show "Success" in STATUS column) kubectl get txn -n <your-namespace> # Check node registrations (all should show publishCount of 2 in Published column) kubectl get reg -n <your-namespace> -
Upgrade with authentication: Once all deployments and registrations are complete, upgrade your Helm deployment with a values file that includes the
rpcAuthconfiguration (see Step 2 below).
Note: If you attempt to deploy with RPC authorization enabled from the start, the operator will be unable to authenticate to the RPC endpoints, causing smart contract deployments, transaction invocations, and node registrations to fail.
Step 1: Create the Credentials Secret¶
First, create a Kubernetes secret containing the credentials file:
# Create the credentials file
htpasswd -cB credentials.htpasswd alice
# Enter password when prompted
# Add additional users (omit -c flag)
htpasswd -B credentials.htpasswd bob
# Enter password when prompted
# Create the Kubernetes secret
kubectl create secret generic paladin-basicauth-credentials \
--from-file=credentials.htpasswd=./credentials.htpasswd \
--namespace=<your-namespace>
Important:
- The secret must be created in the same namespace where your Paladin nodes will be deployed
- The key must be named credentials.htpasswd
- The secret name can be anything you choose (e.g., paladin-basicauth-credentials)
Step 2: Configure in Values File (Upgrade Deployment)¶
After completing the initial deployment and verifying all smart contracts and registrations are complete (as described above), add the RPC authorization configuration to your values-customnet.yaml file and upgrade the deployment:
paladinNodes:
- name: "bank"
# ... other configuration ...
# Enable RPC authorization
rpcAuth:
secretName: paladin-basicauth-credentials # Name of the existing secret
Then upgrade your Helm deployment:
When you configure rpcAuth, the operator automatically:
- Validates that the secret exists and contains the credentials.htpasswd key
- Mounts the credentials file at /rpcauth/credentials.htpasswd in the pod
- Configures the basicauth plugin with the correct path
- Sets rpcServer.authorizers to ["basicauth"]
Using Authentication¶
Once configured, all JSON-RPC requests require valid credentials:
# Make authenticated request
curl -u alice:password \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"account_list","id":1}' \
http://localhost:8545
WebSocket Connections¶
When using WebSocket connections, provide credentials in the initial upgrade request headers:
# Using wscat
wscat -H "Authorization: Basic $(echo -n 'alice:password' | base64)" \
ws://localhost:8545
Implementing Custom Auth Plugins¶
The reference basicauth plugin provides a complete example of implementing an RPC authorization plugin. This section explains how to create your own custom auth plugin following the same pattern.
Plugin Structure¶
An RPC auth plugin must implement the RPCAuthAPI interface with three methods:
ConfigureRPCAuthorizer: Called once at startup to configure the plugin with settingsAuthenticate: Called for each request to verify credentials from HTTP headersAuthorize: Called for each request to check if the authenticated principal is permitted to perform the operation
Implementation Steps¶
Step 1: Create Plugin Directory Structure¶
Create a directory structure following the pattern used by rpcauth/basicauth/:
rpcauth/yourplugin/
├── yourplugin.go # Main entry point with C exports
├── build.gradle # Build configuration
├── go.mod # Go module definition
└── internal/
└── yourplugin/
├── handler.go # Implements RPCAuthAPI interface
├── config.go # Configuration parsing
└── ... # Additional implementation files
Step 2: Implement the RPCAuthAPI Interface¶
Create a handler that implements the RPCAuthAPI interface:
package yourplugin
import (
"context"
"encoding/json"
"fmt"
"github.com/LFDT-Paladin/paladin/toolkit/pkg/plugintk"
"github.com/LFDT-Paladin/paladin/toolkit/pkg/prototk"
)
// YourAuthHandler implements the RPCAuthAPI interface
type YourAuthHandler struct {
// Your plugin state
}
var _ plugintk.RPCAuthAPI = (*YourAuthHandler)(nil)
// ConfigureRPCAuthorizer loads configuration
func (h *YourAuthHandler) ConfigureRPCAuthorizer(
ctx context.Context,
req *prototk.ConfigureRPCAuthorizerRequest,
) (*prototk.ConfigureRPCAuthorizerResponse, error) {
// Parse configuration JSON
var config YourConfig
if err := json.Unmarshal([]byte(req.ConfigJson), &config); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
// Initialize your plugin state
// ...
return &prototk.ConfigureRPCAuthorizerResponse{}, nil
}
// Authenticate verifies credentials from HTTP headers
func (h *YourAuthHandler) Authenticate(
ctx context.Context,
req *prototk.AuthenticateRequest,
) (*prototk.AuthenticateResponse, error) {
// Parse headers JSON
var headers map[string]string
if err := json.Unmarshal([]byte(req.HeadersJson), &headers); err != nil {
return &prototk.AuthenticateResponse{
Authenticated: false,
}, nil
}
// Verify credentials (implement your authentication logic)
// ...
// Return authentication result (format is plugin-specific)
// Example: JSON format
result := map[string]string{"user": "alice", "role": "admin"}
resultJSON, _ := json.Marshal(result)
return &prototk.AuthenticateResponse{
Authenticated: true,
ResultJson: stringPtr(string(resultJSON)),
}, nil
}
// Authorize checks if authenticated principal is permitted
func (h *YourAuthHandler) Authorize(
ctx context.Context,
req *prototk.AuthorizeRequest,
) (*prototk.AuthorizeResponse, error) {
// Parse authentication result (same format you returned from Authenticate)
var authResult map[string]string
if err := json.Unmarshal([]byte(req.ResultJson), &authResult); err != nil {
return &prototk.AuthorizeResponse{
Authorized: false,
}, nil
}
// Check permissions based on:
// - Authentication result (req.ResultJson)
// - RPC method (req.Method)
// - Request payload (req.PayloadJson)
// ...
// Return authorization decision
return &prototk.AuthorizeResponse{
Authorized: true,
}, nil
}
func stringPtr(s string) *string {
return &s
}
Key Points:
- The authentication result format is entirely plugin-specific (JSON, plain string, etc.)
- The same result string returned from Authenticate() is passed to Authorize() unchanged
- Authenticate() returns only authenticated (bool) and optionally result_json (when authenticated=true)
- Authorize() returns only authorized (bool)
- Authentication failures return native HTTP 401 responses (not JSON-RPC errors)
- Authorization failures in WebSocket messages return JSON-RPC error responses
Step 3: Create Main Entry Point¶
Create the main entry point with C exports for the plugin loader:
package main
import (
"C"
"github.com/LFDT-Paladin/paladin/rpcauth/yourplugin/internal/yourplugin"
"github.com/LFDT-Paladin/paladin/toolkit/pkg/plugintk"
)
var ple = plugintk.NewPluginLibraryEntrypoint(func() plugintk.PluginBase {
return plugintk.NewRPCAuthPlugin(func(callbacks plugintk.RPCAuthCallbacks) plugintk.RPCAuthAPI {
return &yourplugin.YourAuthHandler{}
})
})
//export Run
func Run(grpcTargetPtr, pluginUUIDPtr *C.char) int {
return ple.Run(
C.GoString(grpcTargetPtr),
C.GoString(pluginUUIDPtr),
)
}
//export Stop
func Stop(pluginUUIDPtr *C.char) {
ple.Stop(C.GoString(pluginUUIDPtr))
}
func main() {}
Step 4: Configure Build Integration¶
Add your plugin to the Paladin build system:
In build.gradle (root):
def rpcAuths = [
'rpcauth/basicauth/build/libs',
'rpcauth/yourplugin/build/libs', // Add your plugin
]
def assembleSubprojects = [
// ...
':rpcauth:yourplugin', // Add your plugin
]
In Dockerfile:
Create build.gradle in your plugin directory:
ext {
goFiles = fileTree('.') {
include 'internal/**/*.go'
include 'yourplugin.go'
}
}
configurations {
toolkitGo {
canBeConsumed = false
canBeResolved = true
}
goSource {
canBeConsumed = true
canBeResolved = false
}
yourplugin {
canBeConsumed = true
canBeResolved = false
}
}
dependencies {
toolkitGo project(path: ':toolkit:go', configuration: 'goSource')
}
task buildGo(type: GoLib, dependsOn: [':toolkit:go:protoc']) {
inputs.files(configurations.toolkitGo)
baseName 'yourplugin'
sources goFiles
mainFile 'yourplugin.go'
}
task assemble {
dependsOn buildGo
}
dependencies {
yourplugin files(buildGo)
goSource files(goFiles)
}
Step 5: Configure and Use Your Plugin¶
Add your plugin to the Paladin configuration:
{
"rpcAuthorizers": {
"yourplugin": {
"plugin": {
"library": "yourplugin.so",
"type": "c-shared"
},
"config": "{\"yourConfigKey\": \"yourConfigValue\"}"
}
},
"rpcServer": {
"authorizers": ["yourplugin"]
}
}
Reference Implementation¶
The rpcauth/basicauth/ directory provides a complete reference implementation:
basicauth.go: Main entry pointinternal/basicauth/handler.go: ImplementsRPCAuthAPIinterfaceinternal/basicauth/config.go: Configuration parsinginternal/basicauth/authenticator.go: Credential management