Skip to main content

NGINX / Security / Server Setup

NGINX Execute Module: Run Shell Commands via HTTP

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth 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:

  1. A client sends a GET or POST request with a system.run[command] query parameter.
  2. The module URL-decodes the command string.
  3. It forks a child process and executes the command through /bin/sh.
  4. The child process captures both stdout and stderr.
  5. The combined output is returned as a text/html response 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_module directive 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 with bind() failed (13: Permission denied), add the port to the allowed list:

sudo semanage port -a -t http_port_t -p tcp 9191

Avoid 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:

  1. Command timed out. The 3-second alarm killed the child process. Try a simpler, faster command.
  2. Empty command. Sending ?system.run[] crashes the worker process. Always provide a non-empty command.
  3. Missing system.run prefix. The query string must start with system.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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.