Security is paramount when building RPC handlers that interact with the XRP Ledger. Rippled implements a comprehensive role-based access control (RBAC) system to ensure that only authorized clients can execute sensitive operations.
In this section, you'll learn how to properly configure permissions, implement role checks, manage resource limits, and protect your custom handlers from unauthorized access.
The Role Hierarchy
Rippled defines five distinct permission levels:
FORBID < GUEST < USER < IDENTIFIED < ADMIN
Role Definitions
Role
Description
Typical Use Case
FORBID
Blacklisted client
Blocked due to abuse
GUEST
Unauthenticated public access
Public API endpoints, read-only queries
USER
Authenticated client
Standard API operations, account queries
IDENTIFIED
Trusted gateway or service
Transaction submission, privileged reads
ADMIN
Full administrative access
Node management, dangerous operations
Source Location: src/xrpld/core/Config.h
Role Determination
Roles are assigned based on the client's IP address and connection type:
IP-Based Assignment
Configuration
File: rippled.cfg
Assigning Roles to Handlers
When registering a handler, specify the minimum required role:
Example Registrations
Permission Enforcement
The RPC dispatcher automatically enforces role requirements before invoking handlers:
Automatic Check
Manual Check (Inside Handler)
For fine-grained control:
Resource Management
Rippled tracks API usage to prevent denial-of-service attacks:
Resource Charging
Resource Limits
Unlimited Resources
Admin connections have unlimited resources:
IP Whitelisting and Blacklisting
Whitelisting Admin IPs
Blacklisting Abusive Clients
Rippled uses a "Gossip" mechanism to share blacklisted IPs across the network:
Secure Gateway Mode
For production deployments, use secure gateway configuration:
Architecture
Configuration
Benefits:
Rippled only accepts connections from the proxy
Proxy handles TLS termination
Proxy performs initial authentication
Reduces attack surface
Password Authentication (WebSocket)
WebSocket connections support optional password authentication:
Configuration
Client Authentication
Example: Multi-Level Permission Handler
Let's build a handler with different behavior based on role:
Registration:
Behavior:
GUEST: Gets only account and balance
USER: Gets sequence and owner count
IDENTIFIED: Gets flags and previous transaction ID
ADMIN: Gets full administrative details
Best Practices
✅ DO
Always validate roles before sensitive operations
Use the minimum required role for each handler
Charge resources appropriately for expensive queries
Log security events for audit trails
Test with different roles during development
❌ DON'T
Don't hardcode IP addresses in handler code
Don't expose admin functions to lower roles
Don't skip resource charging for expensive operations
Don't leak sensitive information in error messages
Don't trust client-provided role information
Security Checklist
Before deploying a custom handler:
Conclusion
Rippled's authentication and authorization system provides robust protection for the RPC interface through a well-designed role hierarchy. By combining IP-based role assignment, automatic permission enforcement in the dispatcher, resource charging for expensive operations, and fine-grained access control, the system prevents unauthorized access while enabling legitimate use cases. Understanding these security patterns is essential for building handlers that are both functional and secure, and for deploying nodes that safely expose APIs to different client types.
// src/xrpld/core/Config.cpp
Role getRoleFromConnection(
boost::asio::ip::address const& remoteIP,
Port const& port)
{
// Admin IPs have full access
if (config_.ADMIN.contains(remoteIP))
return Role::ADMIN;
// Secure gateway IPs are identified
if (config_.SECURE_GATEWAY.contains(remoteIP))
return Role::IDENTIFIED;
// Check if port requires admin access
if (port.admin_nets && port.admin_nets->contains(remoteIP))
return Role::ADMIN;
// Default to USER for authenticated connections
return Role::USER;
}
# Admin-only access from localhost
[rpc_admin]
admin = 127.0.0.1, ::1
# Trusted gateway access
[secure_gateway]
ip = 192.168.1.100
# Port configuration
[port_rpc_admin_local]
port = 5005
ip = 127.0.0.1
admin = 127.0.0.1
protocol = http
[port_rpc_public]
port = 5006
ip = 0.0.0.0
protocol = http
// src/xrpld/rpc/detail/Handler.cpp
if (context.role < handlerInfo.role) {
return rpcError(rpcNO_PERMISSION,
"You don't have permission for this command");
}
Json::Value doSensitiveOperation(RPC::JsonContext& context)
{
// Check if caller has admin privileges
if (context.role < Role::ADMIN) {
return rpcError(rpcNO_PERMISSION,
"This operation requires admin access");
}
// Additional checks
if (context.role < Role::IDENTIFIED &&
context.params.isMember("dangerous_option"))
{
return rpcError(rpcNO_PERMISSION,
"Only identified users can use this option");
}
// Proceed with operation
// ...
}
// Each request consumes resources
context.consumer.charge(Resource::feeReferenceRPC);
// High-cost operations charge more
if (isExpensiveQuery) {
context.consumer.charge(Resource::feeHighBurdenRPC);
}
// Check if client has exceeded limits
if (!context.consumer.isUnlimited() &&
context.consumer.balance() <= 0)
{
return rpcError(rpcSLOW_DOWN,
"You are making requests too frequently");
}
// Mark a client as abusive
context.netOps.reportAbuse(remoteIP);
// Check if IP is blacklisted
if (context.netOps.isBlacklisted(remoteIP)) {
return rpcError(rpcFORBIDDEN, "Access denied");
}
const ws = new WebSocket('ws://localhost:6006');
ws.send(JSON.stringify({
command: 'login',
user: 'myuser',
password: 'mypassword'
}));
// After successful login, role is elevated to ADMIN
Json::Value doAccountStats(RPC::JsonContext& context)
{
// Basic validation
if (!context.params.isMember(jss::account)) {
return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
}
auto const account = parseBase58<AccountID>(
context.params[jss::account].asString()
);
if (!account) {
return rpcError(rpcACT_MALFORMED);
}
// Get ledger
std::shared_ptr<ReadView const> ledger;
auto const result = RPC::lookupLedger(ledger, context);
if (!ledger) return result;
// Read account
auto const sleAccount = ledger->read(keylet::account(*account));
if (!sleAccount) {
return rpcError(rpcACT_NOT_FOUND);
}
// Build base response (available to all roles)
Json::Value response;
response[jss::account] = to_string(*account);
response["balance"] = to_string(sleAccount->getFieldAmount(sfBalance));
// Add details for USER and above
if (context.role >= Role::USER) {
response["sequence"] = sleAccount->getFieldU32(sfSequence);
response["owner_count"] = sleAccount->getFieldU32(sfOwnerCount);
}
// Add sensitive info for IDENTIFIED and above
if (context.role >= Role::IDENTIFIED) {
response["flags"] = sleAccount->getFieldU32(sfFlags);
response["previous_txn_id"] = to_string(
sleAccount->getFieldH256(sfPreviousTxnID)
);
}
// Add administrative data for ADMIN only
if (context.role >= Role::ADMIN) {
response["ledger_entry_type"] = "AccountRoot";
response["index"] = to_string(keylet::account(*account).key);
}
return response;
}
{
"account_stats",
{
&doAccountStats,
Role::GUEST, // Base access for everyone
RPC::NEEDS_CURRENT_LEDGER
}
}