← Back to DevBytes

FRP Reverse Proxy Security Hardening and Best Practices

Understanding FRP Reverse Proxy

FRP (Fast Reverse Proxy) is an open-source reverse proxy application written in Go that enables you to expose services behind NAT or firewalls to the public internet. It operates on a client-server model: the frps (server) component runs on a publicly accessible machine, while the frpc (client) component runs on the machine behind the firewall that hosts the services you want to expose. The client initiates an outbound connection to the server, and traffic is then tunneled bidirectionally through this established connection.

FRP supports multiple protocol types including TCP, UDP, HTTP, HTTPS, and even STCP (secret TCP) for additional encryption. Its flexibility makes it popular for remote development access, IoT device management, self-hosted service exposure, and bypassing restrictive corporate or residential network policies. However, this very power introduces significant security considerations that must be addressed before deploying FRP in any production or even semi-trusted environment.

Core Components and Data Flow

Before hardening, it is essential to understand how traffic flows. When a user on the internet accesses your-public-server.com:8080, the request reaches frps, which identifies the appropriate tunnel based on the port or virtual host configuration, then forwards the traffic through the persistent connection maintained by frpc. The frpc process receives this traffic and relays it to the actual local service (e.g., a web server on localhost:3000). Responses travel the reverse path back to the original requester.

This architecture means the frps machine is the gateway. If misconfigured, it can become an open proxy allowing unauthorized access to internal networks, or worse, be exploited to attack other hosts.

Why Security Hardening Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

An unsecured FRP deployment is essentially a gaping hole in your network perimeter. The risks include:

Security hardening transforms FRP from a liability into a controlled, auditable, and resilient infrastructure component.

Fundamental Hardening: Authentication Tokens

The first and most critical layer is enforcing strong authentication between frpc and frps. FRP provides a token-based mechanism that must always be configured.

Server-Side Configuration (frps.toml)

# frps.toml — Public-facing server configuration
bindPort = 7000

# Require authentication token from all clients
auth.token = "sk-3a7f8b9c2d1e4f6a8b0c2d4e6f8a0b2c4d"

# Optional: customize token handling behavior
auth.timeout = 10  # seconds to wait for authentication

# Logging for audit trail
log.to = "/var/log/frps/frps.log"
log.level = "info"
log.maxDays = 30

Client-Side Configuration (frpc.toml)

# frpc.toml — Client behind the firewall
serverAddr = "198.51.100.25"
serverPort = 7000

# Must match server token exactly
auth.token = "sk-3a7f8b9c2d1e4f6a8b0c2d4e6f8a0b2c4d"

[[proxies]]
name = "web-app"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 8080

Token best practices: Generate tokens using a cryptographically secure random generator. A minimum length of 32 characters is recommended. Use environment variables or dedicated secret management systems rather than hardcoding tokens in configuration files that may end up in version control.

# Generate a secure token on Linux/macOS
openssl rand -hex 32
# Output: b8a3c1d9e5f2074613a9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d9e0

# Or using Python
python3 -c "import secrets; print(secrets.token_hex(32))"

Transport Encryption: TLS Everywhere

By default, FRP communication between client and server can be plaintext. Enabling TLS ensures confidentiality and integrity of both the control channel and the tunneled data.

Generating TLS Certificates

# Create a self-signed CA and server/client certificates
# CA private key
openssl genrsa -out ca.key 4096

# CA certificate (distribute ca.crt to both sides)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/C=US/ST=State/L=City/O=Org/CN=frp-ca"

# Server private key and certificate
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
  -subj "/C=US/ST=State/L=City/O=Org/CN=frps.example.com"
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
  -set_serial 01 -out server.crt

# Client private key and certificate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
  -subj "/C=US/ST=State/L=City/O=Org/CN=frpc-unique-id"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key \
  -set_serial 02 -out client.crt

Server TLS Configuration

# frps.toml with TLS
bindPort = 7000
auth.token = "sk-3a7f8b9c2d1e4f6a8b0c2d4e6f8a0b2c4d"

# TLS settings
tls.certFile = "/etc/frp/certs/server.crt"
tls.keyFile = "/etc/frp/certs/server.key"
tls.trustedCaFile = "/etc/frp/certs/ca.crt"

# Require client certificates (mutual TLS)
tls.mutual = true

# Enforce TLS for all connections
tls.force = true

Client TLS Configuration

# frpc.toml with TLS
serverAddr = "198.51.100.25"
serverPort = 7000
auth.token = "sk-3a7f8b9c2d1e4f6a8b0c2d4e6f8a0b2c4d"

# TLS client settings
tls.enable = true
tls.certFile = "/etc/frp/certs/client.crt"
tls.keyFile = "/etc/frp/certs/client.key"
tls.trustedCaFile = "/etc/frp/certs/ca.crt"
tls.serverName = "frps.example.com"

[[proxies]]
name = "secure-web"
type = "https"
localIP = "127.0.0.1"
localPort = 443
# Use a different remote port or subdomain
subdomain = "app"

Mutual TLS (mTLS) adds an additional layer where the server verifies the client's certificate before establishing the tunnel. This prevents unauthorized clients even if they somehow obtain the token.

Network-Level Protections

Bind to Specific Interfaces

By default, frps may listen on all interfaces (0.0.0.0). Restrict the bind address to the specific interface that should serve traffic.

# frps.toml — Bind restrictions
bindAddr = "198.51.100.25"  # Only listen on this specific public IP
# or for internal-only proxy:
# bindAddr = "10.0.1.5"

# Separate dashboard binding if needed
dashboardAddr = "127.0.0.1"
dashboardPort = 7500

Firewall Rules (iptables example)

Restrict access to the FRP server port using host-based firewalls. Only allow expected source IPs if possible.

# Allow only specific IP ranges to reach FRP server port 7000
iptables -A INPUT -p tcp --dport 7000 -s 203.0.113.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 7000 -s 198.51.100.5 -j ACCEPT
iptables -A INPUT -p tcp --dport 7000 -j DROP

# For IPv6
ip6tables -A INPUT -p tcp --dport 7000 -s 2001:db8::/32 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 7000 -j DROP

# Save rules (Debian/Ubuntu)
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

Rate Limiting with FRP Built-in Features

FRP supports per-proxy bandwidth and connection limits to prevent resource abuse.

# frps.toml — Global rate limiting
maxPortsPerClient = 10       # Max proxies a single client can register
maxPoolCount = 5             # Max connections in the connection pool
maxPoolIdleTimeout = 60      # Idle connection timeout in seconds

# Per-proxy rate limiting in frpc.toml
[[proxies]]
name = "rate-limited-api"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 8081

# Bandwidth limits (KB/s)
bandwidthLimit = "1MB"       # 1 megabyte per second
bandwidthLimitMode = "server"  # "client" or "server" side enforcement

# Connection limits
maxConnections = 50

Dashboard Security

The FRP dashboard provides visibility into active connections and proxy statistics. By default, it may be accessible without authentication and on all interfaces.

# frps.toml — Secure dashboard configuration
dashboardPort = 7500
dashboardAddr = "127.0.0.1"   # Only accessible locally

# Require authentication for dashboard access
dashboardUser = "admin"
dashboardPwd = "C0mpl3x!D4shb04rd_P@ssw0rd_2024"

# Enable TLS for dashboard (requires cert files)
dashboardTls.certFile = "/etc/frp/certs/server.crt"
dashboardTls.keyFile = "/etc/frp/certs/server.key"
dashboardTls.mutual = false

# Optional: disable dashboard entirely in production
# dashboardPort = 0

If you need remote access to the dashboard, always place it behind a VPN or use SSH port forwarding rather than exposing it directly to the internet.

# Access dashboard via SSH tunnel (run on your local machine)
ssh -L 7500:127.0.0.1:7500 user@frps-server-host
# Then open http://127.0.0.1:7500 in your browser

STCP and XTCP for Additional Encryption Layers

FRP offers STCP (Secret TCP) and XTCP (Encrypted TCP) proxy types that add an encryption layer on top of the tunneled traffic itself, protecting data even if the TLS tunnel were somehow compromised.

# frpc.toml — STCP proxy with shared secret
[[proxies]]
name = "sensitive-db"
type = "stcp"
# STCP secret key — must match on both sides
secretKey = "aes-256-secret-key-must-be-32-bytes-exactly"
localIP = "127.0.0.1"
localPort = 5432

# On the server side or another client that wants to access this service:
# The visitor-side frpc configuration
[[visitors]]
name = "db-visitor"
type = "stcp"
# This visitor connects to the exposed STCP proxy
serverName = "sensitive-db"
secretKey = "aes-256-secret-key-must-be-32-bytes-exactly"
bindAddr = "127.0.0.1"
bindPort = 5432

STCP and XTCP are particularly useful when multiple layers of clients connect through the same frps, and you want end-to-end encryption between the service-hosting client and the service-consuming client without the server being able to decrypt the payload.

Logging and Monitoring for Intrusion Detection

Comprehensive logging is essential for detecting anomalous behavior and potential compromise attempts.

# frps.toml — Advanced logging configuration
log.to = "/var/log/frp/frps.log"
log.level = "info"           # Use "debug" temporarily for troubleshooting
log.maxDays = 30
log.detailed = true          # Include connection metadata

# JSON structured logging for log aggregation systems
log.format = "json"
log.to = "/var/log/frp/frps.json"

Integrate FRP logs with your SIEM or log analysis pipeline. Here is an example using fail2ban to block brute-force authentication attempts:

# /etc/fail2ban/filter.d/frp-auth.conf
[Definition]
failregex = ^.*authentication failed.*from :.*$
            ^.*invalid token.*from :.*$
            ^.*TLS handshake failed.*from :.*$
ignoreregex =

# /etc/fail2ban/jail.d/frp.conf
[frp-auth]
enabled = true
port = 7000
filter = frp-auth
logpath = /var/log/frp/frps.log
maxretry = 5
bantime = 3600   # 1 hour ban
findtime = 600   # 10 minute window

Advanced Hardening Techniques

Minimize Exposed Proxy Footprint

Only expose the absolute minimum set of ports and services. Each exposed proxy increases the attack surface.

# frpc.toml — Principle of least privilege
# Expose ONLY what is necessary, with explicit bind restrictions
[[proxies]]
name = "public-api-only"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 443

# For HTTP proxies, restrict by subdomain or custom domains
[[proxies]]
name = "admin-panel"
type = "http"
localIP = "127.0.0.1"
localPort = 9000
subdomain = "admin"
# Only accessible via admin.example.com if your DNS is configured
# Avoid wildcard subdomains unless absolutely necessary

IP Whitelisting Per Proxy

FRP supports whitelisting source IPs for specific proxies, preventing unauthorized sources from even reaching the tunnel.

# frpc.toml — Per-proxy IP restrictions
[[proxies]]
name = "restricted-service"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 9090

# Only allow connections from these source IPs
whiteList = ["203.0.113.10", "203.0.113.20", "198.51.100.0/24"]

Environment Variable Injection for Secrets

Never store tokens or secrets directly in configuration files. FRP supports environment variable expansion.

# frps.toml using environment variables
bindPort = 7000
auth.token = "${FRP_AUTH_TOKEN}"
tls.certFile = "${FRP_TLS_CERT_FILE}"
tls.keyFile = "${FRP_TLS_KEY_FILE}"
tls.trustedCaFile = "${FRP_TLS_CA_FILE}"

# Start frps with environment variables set
# FRP_AUTH_TOKEN="sk-real-token-value" \
# FRP_TLS_CERT_FILE="/etc/frp/certs/server.crt" \
# FRP_TLS_KEY_FILE="/etc/frp/certs/server.key" \
# FRP_TLS_CA_FILE="/etc/frp/certs/ca.crt" \
# ./frps -c frps.toml

Systemd Service Hardening

When running FRP as a systemd service, apply Linux security modules to restrict the process.

# /etc/systemd/system/frps.service
[Unit]
Description=FRP Server Service
After=network.target
Documentation=https://github.com/fatedier/frp

[Service]
Type=simple
User=frp
Group=frp
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

# Security hardening directives
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadOnlyPaths=/
ReadWritePaths=/var/log/frp /var/run
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
MemoryDenyWriteExecute=yes

# Read-only access to certificate directories
ReadOnlyPaths=/etc/frp/certs
BindReadOnlyPaths=/etc/ssl:/etc/ssl

[Install]
WantedBy=multi-user.target

Regular Rotation of Secrets

Treat tokens and certificates as ephemeral credentials that require periodic rotation. Automate this process using cron or a configuration management tool.

# Monthly token rotation script
#!/bin/bash
# rotate-frp-token.sh
NEW_TOKEN=$(openssl rand -hex 32)
CONFIG_FILE="/etc/frp/frps.toml"

# Update token in configuration (using env var approach is safer)
sed -i "s/^auth\.token = .*/auth.token = \"${NEW_TOKEN}\"/" "$CONFIG_FILE"

# Restart service gracefully
systemctl reload-or-restart frps

# Distribute new token to authorized clients via secure channel
# e.g., ansible vault, hashicorp vault, or secure scp
echo "New token: ${NEW_TOKEN}" | \
  age -r age1publickey... | \
  ssh client-host "age -d > /etc/frp/token.env"

Common Attack Patterns and Mitigations

Replay Attacks

Without TLS and proper nonce handling, an attacker could capture and replay authentication packets. Mitigation: Always enable TLS with unique session keys. FRP's TLS implementation generates fresh session keys per connection, rendering captured packets useless.

Token Brute-Forcing

An attacker probing the FRP port repeatedly with different tokens. Mitigation: Use fail2ban as shown above, implement firewall rate limiting, and use long random tokens (64+ characters) that are computationally infeasible to guess.

Dashboard Information Disclosure

The dashboard reveals all active proxies and their endpoints. Mitigation: Bind the dashboard to localhost only, require strong authentication, or disable it entirely in production. If exposed, an attacker gains a complete map of your internal services.

Proxy Amplication

A compromised client registering hundreds of proxies to exhaust server resources. Mitigation: Set maxPortsPerClient to a low value (5-10) and monitor for abnormal proxy registration spikes.

Complete Secure Configuration Example

Below is a production-ready, hardened configuration combining all the practices discussed.

frps.toml (Server)

# =============================================
# FRP Server - Hardened Production Configuration
# =============================================

# Binding
bindAddr = "203.0.113.50"
bindPort = 7000

# Transport security
tls.force = true
tls.mutual = true
tls.certFile = "/etc/frp/certs/server.crt"
tls.keyFile = "/etc/frp/certs/server.key"
tls.trustedCaFile = "/etc/frp/certs/ca.crt"

# Authentication
auth.token = "${FRP_AUTH_TOKEN}"
auth.timeout = 5

# Resource control
maxPortsPerClient = 5
maxPoolCount = 10
maxPoolIdleTimeout = 30

# Dashboard - localhost only with authentication
dashboardAddr = "127.0.0.1"
dashboardPort = 7500
dashboardUser = "${FRP_DASHBOARD_USER}"
dashboardPwd = "${FRP_DASHBOARD_PASSWORD}"

# Logging
log.to = "/var/log/frp/frps.log"
log.level = "info"
log.maxDays = 30
log.format = "json"
log.detailed = true

# Allow only specific ports to be exposed remotely
allowPorts = [
  { start = 8000, end = 8100 },
  { start = 443, end = 443 }
]

frpc.toml (Client)

# =============================================
# FRP Client - Hardened Production Configuration
# =============================================

serverAddr = "203.0.113.50"
serverPort = 7000

# Authentication
auth.token = "${FRP_AUTH_TOKEN}"

# Transport security
tls.enable = true
tls.mutual = true
tls.certFile = "/etc/frp/certs/client.crt"
tls.keyFile = "/etc/frp/certs/client.key"
tls.trustedCaFile = "/etc/frp/certs/ca.crt"
tls.serverName = "frps.example.com"

# Proxies — only expose what is necessary
[[proxies]]
name = "public-website"
type = "https"
localIP = "127.0.0.1"
localPort = 443
subdomain = "www"
bandwidthLimit = "10MB"
maxConnections = 100

[[proxies]]
name = "internal-api"
type = "stcp"
secretKey = "${STCP_SECRET_KEY}"
localIP = "127.0.0.1"
localPort = 8080
whiteList = ["10.0.0.0/8", "172.16.0.0/12"]

Operational Checklist

Before deploying FRP to production, verify every item on this checklist:

Conclusion

FRP is an exceptionally useful tool for bridging network boundaries, but its power demands respect. A default installation without hardening is an open invitation to attackers. By implementing the layered security model described in this tutorial — strong authentication, mutual TLS, network restrictions, rate limiting, dashboard protection, logging, and system-level hardening — you transform FRP into a robust, production-grade reverse proxy that you can trust with your internal services. Security is not a one-time configuration task; it requires ongoing vigilance. Regularly audit your configuration, rotate secrets, review logs for anomalies, and stay informed about FRP security advisories. With these practices in place, FRP becomes not a liability but a secure, flexible component of your infrastructure.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles