yum upgrades for production use, this is the repository for you.
Active subscription is required.
The NGINX execute module lets you run shell commands directly through HTTP requests and return the output as a response. While running arbitrary commands from a web server sounds dangerous — and it is, if misconfigured — there are legitimate scenarios where this capability simplifies system administration. Health checks, quick diagnostics on private networks, and internal automation tools all benefit from the ability to trigger a shell command through a simple HTTP call.
In this guide, you will learn how to install the NGINX execute module, lock it down with IP-based restrictions, and understand the security implications before deploying it on any server. Every configuration example has been tested on a clean Rocky Linux 10 server.
How the NGINX Execute Module Works
The module registers a content handler for any location block where you enable it. When a request arrives at that location, it extracts a command from the query string, executes it via /bin/sh -c, and returns the output as the HTTP response body.
Here is the request flow:
- A client sends a GET or POST request with a
system.run[command]query parameter. - The module URL-decodes the command string.
- It forks a child process and executes the command through
/bin/sh. - The child process captures both stdout and stderr.
- The combined output is returned as a
text/htmlresponse with HTTP 200 status.
The module enforces a 3-second timeout on every command execution. If a command runs longer than 3 seconds, NGINX terminates the child process. This prevents runaway commands from blocking the worker process indefinitely.
Additionally, command output is limited to 512 KB. Commands that produce more output will be truncated at this limit.
Supported HTTP Methods
The module accepts three HTTP methods:
- GET — the most common method for executing commands
- POST — useful when commands contain special characters that are difficult to URL-encode
- HEAD — returns headers only, without executing the command’s output phase
All other HTTP methods receive a 405 Method Not Allowed response.
Command Syntax
Commands are passed via the query string using a specific format:
?system.run[command_here]
For commands with spaces or special characters, use URL encoding:
?system.run[df%20-h%20/]
The square brackets are part of the syntax — they delimit the command parameter. The module parses system.run as the key and everything between [ and ] as the command to execute.
Security Warning
The NGINX execute module allows arbitrary command execution on your server. Any command that the NGINX worker user can run becomes accessible through HTTP. This includes reading files, listing directories, inspecting processes, and potentially modifying system state.
Never expose this module to the public internet without strict access controls. An unprotected endpoint gives attackers full shell access as the nginx user. Even with limited privileges, an attacker could:
- Read sensitive configuration files (
/etc/passwd, application configs) - Enumerate running processes and network connections
- Download and execute malicious payloads
- Pivot to other services on the same network
- Exhaust server resources with resource-intensive commands
This module is intended for private networks, internal tooling, and development environments only. If you must use it in production, apply every security measure described in this guide.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-execute
Then add the following line at the top of /etc/nginx/nginx.conf, before any http, events, or other blocks:
load_module modules/ngx_http_execute_module.so;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-execute
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
After installation on any platform, verify that NGINX accepts the configuration:
sudo nginx -t
Configuration Reference
The NGINX execute module provides a single directive.
command
Syntax: command on | off;
Default: —
Context: location
Enables or disables command execution for a specific location block. When set to on, the module registers its content handler and processes incoming requests. When set to off (or omitted), the location behaves normally.
location /exec {
command on;
}
The directive accepts only on as a value to enable execution. Any other value (including off) leaves the handler unregistered, which means the location falls through to the default content handler.
Important: The command directive sets a content handler, which means it replaces any other content handler for that location. You cannot combine command on with proxy_pass, fastcgi_pass, return, or try_files in the same location block.
Basic Configuration
Here is a minimal working configuration that enables command execution on a specific path:
server {
listen 80;
server_name localhost;
location /exec {
command on;
}
location / {
return 200 "default page\n";
}
}
After reloading NGINX, test the module with curl:
curl -g "http://localhost/exec?system.run[hostname]"
The -g flag disables curl’s globbing feature, which would otherwise interpret the square brackets as a range pattern. You must use -g with every curl command that contains square brackets.
Example output:
localhost.localdomain
More Command Examples
# Check disk usage
curl -g "http://localhost/exec?system.run[df%20-h%20/]"
# Show current date and time
curl -g "http://localhost/exec?system.run[date]"
# List running processes
curl -g "http://localhost/exec?system.run[ps%20aux%20|%20head%20-10]"
# Check system uptime
curl -g "http://localhost/exec?system.run[uptime]"
# Show kernel version
curl -g "http://localhost/exec?system.run[uname%20-a]"
Commands run as the nginx user by default. The output includes both stdout and stderr, so error messages appear in the response as well.
Security Hardening
Because the NGINX execute module grants shell access via HTTP, you must apply multiple layers of protection. The following sections describe essential security measures.
Restrict Access by IP Address
The most important security measure is restricting which IP addresses can reach the execute endpoint. Use NGINX’s built-in allow and deny directives:
location /exec {
allow 127.0.0.1;
allow ::1;
deny all;
command on;
}
This configuration permits only localhost connections. For a private network, add your internal subnet:
location /exec {
allow 127.0.0.1;
allow ::1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
command on;
}
NGINX evaluates allow and deny directives during the access phase, before the content handler runs. A denied request receives a 403 Forbidden response and no command is executed.
For more advanced variable-based access control rules, consider the NGINX access control module.
Use a Non-Obvious Location Path
Avoid predictable paths like /exec, /shell, or /command. Use a randomized or obscure path that is difficult to guess:
location /internal-a7f3b2c9d1e8 {
allow 127.0.0.1;
allow ::1;
deny all;
command on;
}
This is security through obscurity — it should supplement, not replace, IP-based restrictions. However, it prevents automated scanners from discovering the endpoint through common path dictionaries.
Bind to a Non-Standard Port
Listen on a port that is only accessible from your internal network. Combine this with firewall rules to block external access:
server {
listen 127.0.0.1:9191;
server_name localhost;
location /exec {
command on;
}
}
Binding to 127.0.0.1:9191 ensures the server only accepts connections from localhost. No firewall rule is needed because the socket is not bound to an external interface.
SELinux note: On RHEL-based systems with SELinux enforcing, NGINX can only bind to ports labeled
http_port_t. If NGINX fails to start withbind() failed (13: Permission denied), add the port to the allowed list:sudo semanage port -a -t http_port_t -p tcp 9191Avoid port 9090, which is commonly used by Cockpit Web Console on RHEL, Rocky Linux, and AlmaLinux.
Combine with Rate Limiting
Prevent abuse by applying rate limiting to the execute endpoint. NGINX’s built-in limit_req module restricts how many requests a client can make:
limit_req_zone $binary_remote_addr zone=exec_limit:1m rate=5r/m;
server {
listen 80;
server_name localhost;
location /exec {
limit_req zone=exec_limit burst=2 nodelay;
allow 127.0.0.1;
allow ::1;
deny all;
command on;
}
}
This limits the endpoint to 5 requests per minute per IP address, with a burst allowance of 2 additional requests. For stricter control, consider the NGINX dynamic limit req module which stores rate limit state in Redis for distributed environments.
Disable in Production Deployments
If you use the execute module only during development or debugging, ensure it is disabled in production configurations. Create a separate configuration file that is only included in development:
# /etc/nginx/conf.d/execute-dev.conf
# Only include this file in development environments
server {
listen 127.0.0.1:9191;
location /exec {
command on;
}
}
Remove or rename this file before deploying to production.
Practical Use Cases
Despite the security risks, there are legitimate applications for the execute module in controlled environments.
Health Check Endpoint
Create a health check endpoint that verifies system services are running:
server {
listen 127.0.0.1:9191;
location /health {
command on;
}
}
Then query it from your monitoring system:
# Check available disk space
curl -g "http://localhost:9191/health?system.run[df%20-h%20/%20|%20tail%20-1]"
# Check memory usage
curl -g "http://localhost:9191/health?system.run[free%20-m%20|%20head%20-2]"
# Check CPU load average
curl -g "http://localhost:9191/health?system.run[cat%20/proc/loadavg]"
For production health checks, consider using NGINX’s built-in stub_status module or the NGINX active health checks module instead, as they do not involve shell execution.
Quick Server Diagnostics
During incident response, the execute module provides rapid access to server state without needing SSH:
# Check recent error log entries
curl -g "http://localhost:9191/exec?system.run[tail%20-5%20/var/log/nginx/error.log]"
# Check who is consuming CPU
curl -g "http://localhost:9191/exec?system.run[ps%20aux%20--sort=-%25cpu%20|%20head%20-5]"
# Check listening ports
curl -g "http://localhost:9191/exec?system.run[cat%20/proc/net/tcp%20|%20wc%20-l]"
Internal Automation Triggers
Trigger scripts on a server through HTTP calls from CI/CD pipelines or orchestration tools:
# Trigger a cache purge script
curl -g "http://internal-server:9191/exec?system.run[/usr/local/bin/purge-cache.sh]"
# Rotate log files
curl -g "http://internal-server:9191/exec?system.run[/usr/local/bin/rotate-logs.sh]"
Important: For production automation, dedicated tools like Ansible, Salt, or systemd service triggers are more secure and auditable than the execute module.
Known Limitations
There are several limitations you should understand before deploying this module.
3-Second Hard Timeout
Every command is subject to a 3-second alarm() timeout. If a command does not complete within 3 seconds, the module sends SIGTERM to the child process group and terminates execution. You cannot configure this timeout — it is hardcoded in the module source.
Commands that consistently need more than 3 seconds will not work. Long-running operations should be triggered asynchronously, for example by writing a flag file that a background service picks up.
No Interactive Commands
Commands that require user interaction (such as top, vi, or less) will not work. The module captures output from a non-interactive shell — there is no terminal attached.
Commands Run as the NGINX Worker User
All commands execute as the user running the NGINX worker processes, typically nginx. This user has limited permissions by default:
curl -g "http://localhost/exec?system.run[whoami]"
# Output: nginx
The nginx user cannot modify system files, install packages, or access other users’ home directories. However, it can read world-readable files and potentially access application data.
No Request Body Processing
The module extracts commands only from the query string (?system.run[...]). It does not read or process the HTTP request body, even for POST requests. POST and GET requests use the same query string mechanism.
Empty Commands Cause Instability
Sending an empty command (?system.run[]) triggers a memory error that crashes the NGINX worker process. The master process spawns a replacement worker, so the server recovers automatically, but this behavior can be exploited for denial-of-service attacks. This is another reason to restrict access to the endpoint.
Alternatives to Consider
Before deploying the NGINX execute module, evaluate whether one of these alternatives better fits your requirements.
NGINX Stub Status (Built-in)
For basic health monitoring, NGINX’s built-in stub_status module provides connection and request metrics without shell execution:
location /nginx-status {
stub_status;
allow 127.0.0.1;
deny all;
}
CGI/FastCGI Scripts
For controlled command execution, use a CGI or FastCGI script that validates inputs and restricts which commands can run. This approach offers input sanitization and audit logging that the execute module lacks.
SSH-Based Remote Execution
For server management from CI/CD pipelines or orchestration tools, SSH provides encrypted, authenticated, and auditable remote command execution. Tools like Ansible use SSH under the hood.
NGINX Lua Module
The NGINX Lua module provides os.execute() and io.popen() within Lua scripts, giving you the ability to validate inputs, sanitize commands, and control output formatting before executing anything.
Performance Considerations
The NGINX execute module has a moderate performance impact because it forks a child process for every request. Here is what this means in practice:
- Process creation overhead. Each request triggers a
fork()+exec()system call. On modern Linux systems, this takes approximately 1-2 milliseconds. - Worker blocking. The NGINX worker process blocks while waiting for command output. During this time, the worker cannot process other requests assigned to it. This is significant because NGINX workers are designed to handle thousands of concurrent connections using non-blocking I/O.
- Memory impact. Each forked process inherits the NGINX worker’s memory space (via copy-on-write). For commands that allocate significant memory, this can temporarily increase server memory usage.
- Concurrency limit. With the default 3-second timeout, each worker process can handle at most one execute request every 3 seconds. If you have 4 worker processes and all are busy with execute requests, additional requests will queue.
Do not use the execute module for high-traffic endpoints. It is designed for occasional administrative use, not for serving application requests. For high-throughput scenarios, use FastCGI, proxy_pass to a dedicated application server, or the Lua module.
Troubleshooting
“unknown directive ‘command'”
The module is not loaded. Verify the load_module directive is present at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_execute_module.so;
Verify the module file exists:
ls /usr/lib64/nginx/modules/ngx_http_execute_module.so
Empty Response or Connection Reset
If curl returns an empty response or connection reset, the most likely causes are:
- Command timed out. The 3-second alarm killed the child process. Try a simpler, faster command.
- Empty command. Sending
?system.run[]crashes the worker process. Always provide a non-empty command. - Missing
system.runprefix. The query string must start withsystem.run[. Other formats are not recognized.
403 Forbidden
Your IP address is not in the allow list. Check the allow and deny directives in the location block and verify your client IP:
curl -s -o /dev/null -w "%{http_code}" -g "http://server/exec?system.run[hostname]"
If you get 403, add your IP to the allow list and reload NGINX.
“bind() failed (13: Permission denied)”
On RHEL-based systems with SELinux enforcing, NGINX cannot bind to ports that are not labeled http_port_t. Add the port:
sudo semanage port -a -t http_port_t -p tcp 9191
Then reload NGINX.
Command Output Is Truncated
The module caps output at 512 KB. For commands with large output, pipe through head or tail:
curl -g "http://localhost/exec?system.run[journalctl%20--no-pager%20|%20tail%20-50]"
Permission Denied Errors in Output
Commands run as the nginx user. If the output contains “Permission denied”, the command requires privileges that the nginx user does not have. Do not run NGINX as root to work around this — instead, use sudo with a carefully scoped sudoers rule if absolutely necessary.
Conclusion
The NGINX execute module provides a straightforward way to run shell commands via HTTP requests. It is useful for internal health checks, quick server diagnostics, and automation triggers on private networks. However, the module’s design prioritizes simplicity over security — it offers no input validation, no command whitelisting, and no audit logging.
Always restrict access with IP-based rules (allow/deny), bind to localhost or internal interfaces, and apply rate limiting. Never expose the execute endpoint to untrusted networks. For production automation, prefer dedicated tools like Ansible, Salt, or SSH-based scripts that provide proper authentication and audit trails.
Install the NGINX execute module from the GetPageSpeed repository for RPM-based or APT-based systems. The module source code is available on GitHub.
