WordPress REST API Authentication Hardening: IP Allowlisting and Rate Limiting

The WordPress REST API is enabled for all visitors by default, and while most endpoints require authentication for write operations, the discovery endpoint, user enumeration via /wp/v2/users, and any publicly registered route can leak information or become a target for brute-force and enumeration attacks. Hardening the REST API requires a layered approach: disabling public discovery, requiring authentication for sensitive namespaces, adding request-rate limiting, and optionally restricting access by IP.

Problem: A WordPress site's REST API is open to unauthenticated requests from any IP — scrapers, credential-stuffing bots, and enumeration attacks target /wp-json/wp/v2/users and other sensitive endpoints at scale.

Solution: Restrict the users endpoint by removing it in the rest_endpoints filter. Add IP-based allowlisting in Nginx for the /wp-json/ location block for sensitive routes. Apply rate limiting with limit_req_zone for the entire REST API path. Require authentication on custom routes with a strict permission_callback that returns new WP_Error('rest_forbidden') for unauthenticated requests.


The code below removes the REST API link from the frontend for unauthenticated users, blocks /wp/v2/users enumeration, adds a simple rate-limiter using transients, and shows an Nginx-level IP restriction pattern for admin-only REST namespaces.


 and HTTP headers for guests
add_action( 'init', function () {
    if ( ! is_user_logged_in() ) {
        remove_action( 'wp_head',             'rest_output_link_wp_head',       10 );
        remove_action( 'template_redirect',   'rest_output_link_header',        11 );
        remove_action( 'xmlrpc_rsd_apis',     'rest_output_rsd'                    );
    }
} );

// 2. Block user enumeration via REST API for unauthenticated requests
add_filter( 'rest_endpoints', function ( array $endpoints ): array {
    if ( ! is_user_logged_in() ) {
        unset( $endpoints['/wp/v2/users']      );
        unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
    }
    return $endpoints;
} );

// 3. Require authentication for all REST API requests (optional — breaks Gutenberg if on frontend)
//    Use only for REST-only headless setups or wp-admin REST calls
add_filter( 'rest_authentication_errors', function ( $result ) {
    if ( ! empty( $result ) ) {
        return $result;   // existing auth error — pass through
    }
    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_not_logged_in',
            'REST API requires authentication.',
            [ 'status' => 401 ]
        );
    }
    return $result;
} );

// 4. Rate-limit REST API requests per IP (transient-based, simple)
add_filter( 'rest_pre_dispatch', function ( $result, WP_REST_Server $server, WP_REST_Request $request ) {
    $ip       = sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? '' );
    $key      = 'rest_rate_' . md5( $ip );
    $window   = 60;   // seconds
    $max_req  = 60;   // requests per window

    $count = (int) get_transient( $key );
    if ( $count >= $max_req ) {
        return new WP_Error( 'rest_rate_limited', 'Too many requests.', [ 'status' => 429 ] );
    }
    if ( $count === 0 ) {
        set_transient( $key, 1, $window );
    } else {
        set_transient( $key, $count + 1, $window );
    }
    return $result;
}, 10, 3 );


# Nginx: restrict /wp-json/my-plugin/ to specific IPs only
location ~ ^/wp-json/my-plugin/ {
    allow 192.168.1.0/24;
    allow 203.0.113.42;
    deny  all;

    try_files $uri $uri/ /index.php?$args;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
}


NOTE: Applying the rest_authentication_errors filter site-wide breaks the block editor for logged-out preview links and REST-dependent plugins like WooCommerce Blocks — scope the authentication requirement to specific namespaces using $request->get_route() and only enforce it for your custom API endpoints.