advanced 60 min

RPC authentication & error handling

Role-based access control and the RPC error-code system.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain the `Role` enum and role checks.
  • Use the `ErrorCodeI` error codes and `rpcError`.
  • Validate input and map errors to responses.
Complete this module by self-assessment and a quiz. Jump to assessment

Securing Your RPC Handlers with Role-Based Access Control


Introduction

≈60 min · Advanced · builds on RPC architecture & request flow

Not every caller should be able to do everything, and every failure needs a clear answer. In this module you'll learn rippled's role-based access control (the Role enum and how roles get assigned), the standardized error-code system, and how handlers charge resources to keep abusive clients in check. Security and good error hygiene, side by side.


The Role Hierarchy

In brief: the Role levels (admin, user, and so on) that gate what a caller may do.

Rippled defines five distinct permission levels:

The role ladder, five permission levels in increasing privilege: FORBID (blacklisted), GUEST (anonymous), USER (standard client), IDENTIFIED (known identity, higher limits), ADMIN (full control)

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

Key idea. RPC security is role-based: the same command can be allowed for an admin and refused for an anonymous caller. The role, not the command, is the gate.


Role Determination

In brief: how a request's role is decided, often from its source IP or the config.

How a request gets its role: the port's admin allowance for the source IP and the credentials decide between ADMIN and FORBID, a secure_gateway proxy yields IDENTIFIED, and everything else lands on USER or GUEST.

Roles are assigned based on the client's IP address and connection type:

IP-Based Assignment

Configuration

File: xrpld.cfg


Assigning Roles to Handlers

In brief: declaring the minimum role in the handler table.

When registering a handler, specify the minimum required role:

Example Registrations


Permission Enforcement

In brief: how handlers actually enforce the role a command requires.

The RPC dispatcher automatically enforces role requirements before invoking handlers:

Automatic Check

// src/xrpld/rpc/detail/Handler.cpp
if (context.role < handlerInfo.role) {
    return rpcError(RpcNoPermission,
        "You don't have permission for this command");
}

Manual Check (Inside Handler)

For fine-grained control:


Resource Management

In brief: charging resources to throttle abusive clients.

Rippled tracks API usage to prevent denial-of-service attacks:

Resource Charging

// Each request consumes resources
context.consumer.charge(Resource::feeReferenceRPC);

// High-cost operations charge more
if (isExpensiveQuery) {
    context.consumer.charge(Resource::feeHighBurdenRPC);
}

Resource Limits

// Check if client has exceeded limits
if (!context.consumer.isUnlimited() &&
    context.consumer.balance() <= 0)
{
    return rpcError(RpcSlowDown,
        "You are making requests too frequently");
}

Unlimited Resources

Admin connections have unlimited resources:

bool isUnlimited() const
{
    return role_ >= Role::ADMIN;
}

IP Whitelisting and Blacklisting

In brief: who may even knock on the admin door.

Whitelisting Admin IPs

# xrpld.cfg
[rpc_admin]
admin = 127.0.0.1
admin = 192.168.1.50
admin = ::1

Blacklisting Abusive Clients

Rippled uses a "Gossip" mechanism to share blacklisted IPs across the network:

// Mark a client as abusive
context.netOps.reportAbuse(remoteIP);

// Check if IP is blacklisted
if (context.netOps.isBlacklisted(remoteIP)) {
    return rpcError(RpcForbidden, "Access denied");
}

Secure Gateway Mode

In brief: letting a trusted proxy vouch for identified users.

For production deployments, use secure gateway configuration:

Architecture

Client → Reverse Proxy (nginx) → Rippled
         [IP: 192.168.1.100]      [Trusted]

Configuration

[secure_gateway]
ip = 192.168.1.100

[port_rpc]
port = 5005
ip = 127.0.0.1
protocol = http

Benefits:

  • Rippled only accepts connections from the proxy
  • Proxy handles TLS termination
  • Proxy performs initial authentication
  • Reduces attack surface

Password Authentication (WebSocket)

In brief: admin credentials on the socket, and their limits.

WebSocket connections support optional password authentication:

Configuration

[rpc_startup]
{ "command": "log_level", "severity": "warning" }

[port_ws_admin_local]
port = 6006
ip = 127.0.0.1
admin = 127.0.0.1
protocol = ws
admin_user = myuser
admin_password = mypassword

Client Authentication

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

Example: Multi-Level Permission Handler

In brief: one handler whose answer grows with the caller's role.

Let's build a handler with different behavior based on role:

Registration:

{
    "account_stats",
    {
        &doAccountStats,
        Role::GUEST,  // Base access for everyone
        Condition::NeedsCurrentLedger
    }
}

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:

[ ] Minimum role correctly assigned in handler table
[ ] Input validation prevents injection attacks
[ ] Resource charging implemented for expensive operations
[ ] Sensitive data not exposed to unauthorized roles
[ ] Error messages don't leak system information
[ ] Tested with GUEST, USER, and ADMIN roles
[ ] Logs security-relevant events
[ ] Follows principle of least privilege

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.


Error Handling and Validation

In brief: the error contract: shape, codes, and honesty.

Building Robust Handlers with Comprehensive Error Management


Introduction

The difference between a fragile handler and a production-ready one lies in proper error handling and input validation. Every RPC handler must anticipate failures, invalid input, missing resources, permission issues, and unexpected edge cases, and respond with clear, actionable error messages.

In this section, you'll learn the complete error handling framework used throughout Rippled, including standard error codes, HTTP status mapping, input validation patterns, and strategies for protecting sensitive data while providing useful debugging information.


RPC Error Codes

In brief: the code families and when each fires.

Rippled defines a comprehensive set of error codes for different failure scenarios:

Standard Error Codes

Source Location: include/xrpl/protocol/ErrorCodes.h

Complete Error Code List

For a comprehensive list of all error codes and their meanings:

enum ErrorCodeI {
    RpcUnknown = -1,
    RpcSuccess = 0,
    RpcBadSyntax = 1,
    RpcNoCurrent = 16,
    RpcActNotFound = 19,
    RpcInvalidParams = 31,
    RpcUnknownCommand = 32,
    RpcActMalformed = 35,
    RpcInternal = 73,
    // ... many more defined
};

HTTP Status Code Mapping

RPC errors must map to appropriate HTTP status codes:

Mapping Strategy

Example HTTP Response

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "result": {
        "status": "error",
        "error": "invalid_params",
        "error_code": -32602,
        "error_message": "Missing required field: 'account'"
    }
}

Error Response Formatting

Standard Error Response Structure

Every error response follows this format:

{
    "result": {
        "status": "error",
        "error": "error_code_name",
        "error_code": -32602,
        "error_message": "Human-readable error description",
        "request": {
            "command": "the_command_that_failed",
            "... ": "request parameters (sanitized)"
        }
    }
}

Building Error Responses in Code

Source Location: src/xrpld/rpc/detail/RPCHelpers.h


Input Validation Patterns

In brief: presence, type, bounds: the order that never changes.

Validate Required Fields

Validate Numeric Ranges

Validate Addresses and Identifiers

Validate Enum/Choice Parameters

std::string command = context.params[jss::command].asString();

static constexpr std::array<std::string_view, 3> validCommands = {
    "buy", "sell", "cancel"
};

if (std::find(validCommands.begin(), validCommands.end(), command)
    == validCommands.end())
{
    return rpcError(RpcInvalidParams,
        "command must be 'buy', 'sell', or 'cancel'");
}

Validate Optional Fields


Sensitive Data Masking

In brief: what must never appear in logs or error messages.

Protect Private Keys and Secrets

// NEVER expose private keys in responses
Json::Value response;

// Bad: Never do this
// response["private_key"] = account.getPrivateKey();

// Good: Omit sensitive data entirely
response[jss::account] = to_string(accountID);
response[jss::public_key] = to_string(publicKey);

Sanitize Error Messages

// Bad: Leaks information about internal structure
if (database.query(accountID) == nullptr) {
    return rpcError(RpcActNotFound,
        "SELECT * FROM accounts WHERE id = " + std::to_string(accountID)
        + " returned no rows");
}

// Good: Hide implementation details
if (database.query(accountID) == nullptr) {
    return rpcError(RpcActNotFound,
        "Account not found");
}

Mask Sensitive Request Data


Exception Handling

In brief: catching everything so the caller sees an error object, not a crash.

Catch Exceptions in Handlers

Standard Exception Types


Comprehensive Validation Example

Here's a complete example showing all validation patterns:


Validation Best Practices

DO

  • Validate early and often, Check all inputs before processing
  • Use specific error messages, Help clients understand what went wrong
  • Validate all numeric bounds, Prevent overflow, underflow, and resource exhaustion
  • Check account existence, Before attempting operations
  • Log validation failures, For security monitoring and debugging
  • Fail fast, Return errors as soon as validation fails

DON'T

  • Trust client input, Always validate, even if it looks correct
  • Expose internal errors, Sanitize error messages
  • Allow injection attacks, Escape or validate all string inputs
  • Leak sensitive data, Never include secrets in responses
  • Ignore format validation, Invalid formats can cause crashes
  • Disable validation for "trusted" clients, All clients need validation

Common Validation Scenarios

Scenario 1: Currency/Amount Handling

Scenario 2: Ledger Index Selection

Scenario 3: Pagination Validation


Conclusion

Comprehensive error handling and input validation separate production-quality handlers from fragile prototypes. Rippled's error framework provides specific codes for every failure scenario, proper HTTP status mapping, and patterns for protecting sensitive information while giving clients actionable feedback. By validating inputs early, handling exceptions gracefully, and following the principle of failing fast, handlers become robust against malformed requests, edge cases, and potential attacks. These practices are fundamental for any handler that will face real-world traffic.


Summary

This module covered access control and errors for the RPC layer. Security is role-based: the Role of a request (often decided from its source IP or config) gates what it may do, so the same command can be allowed for an admin and refused for an anonymous caller. You also worked with the standardized error codes and saw how resource charging throttles abusive clients.

To remember:

  • The Role is derived from source IP / port config (src/xrpld/rpc/Role.h)
  • Admin = requests arriving on an admin-configured interface (admin= IPs on the port stanza)
  • Handlers declare a minimum role in the table; enforcement happens before invocation
  • Errors: include/xrpl/protocol/ErrorCodes.h + rpcError() for standard shapes
  • Resource charging (Resource::Charge) throttles abusive clients
  • secure_gateway lets a trusted proxy forward caller identity
  • Admin-only examples: get_counts, stop, validators, consensus_info
  • Watch out: binding an admin port to a public interface is the number-one operational hole; keep admin on localhost

Next up. You know the rules of the road. Time to drive: next you build your own RPC command from scratch.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026