NGINX Unit
v. 1.24.0

Security Checklist§

At its core, Unit has security as one of its top priorities; our development follows the appropriate best practices focused on making the code robust and solid. However, even the most hardened system requires proper setup, configuration, and maintenance.

This guide lists the steps to protect your Unit from installation to individual app configuration.

Update Unit Regularly§

Rationale: Each release introduces bug fixes and new features that improve your installation’s security.

Actions: Follow our latest news and upgrade to new versions shortly after they are released.

Details

Specific upgrade steps depend on your installation method:

  • The recommended option is to use our official packages or Docker images; with them, it’s just a matter of updating unit-* packages with your package manager of choice or switching to a newer image.
  • If you use a third-party installation method, consult the maintainer’s documentation for details.
  • If you install Unit from source files, rebuild and reinstall Unit and its modules from scratch.

Secure Socket and State§

Rationale: Your control socket and state directory provide unlimited access to Unit’s configuration, which calls for stringent protection.

Actions: Default configuration in our official packages is usually sufficient; if you use another installation method, ensure the control socket and the state directory are safe.

Control Socket

If you use a Unix control socket, ensure it is available to root only:

$ unitd -h

      ...
      --control ADDRESS    set address of control API socket
                           default: "unix:/path/to/control.unit.sock"

# ls -l /path/to/control.unit.sock

      srw------- 1 root root 0 ... /path/to/control.unit.sock

Unix domain sockets aren’t network accessible; for remote access, use NGINX or a solution such as SSH:

$ ssh -N -L ./here.sock:/path/to/control.unit.sock root@unit.example.com &
$ curl --unix-socket ./here.sock

      {
          "certificates": {},
          "config": {
              "listeners": {},
              "applications": {}
          }
      }

If you prefer an IP-based control socket, avoid public IPs; they expose the control API and all its capabilities. This means your Unit instance can be manipulated by whoever is physically able to connect:

# unitd --control 10.1.1.14:8080
$ curl 10.1.1.14:8080

      {
          "certificates": {},
          "config": {
              "listeners": {},
              "applications": {}
          }
      }

Instead, opt for the loopback address to ensure all access is local to your server:

# unitd --control 127.0.0.1:8080
$ curl 10.1.1.14:8080

    curl: (7) Failed to connect to 10.1.1.14 port 8080: Connection refused

However, any processes local to the same system can access the local socket, which calls for additional measures. A go-to solution would be using NGINX to proxy Unit’s control API.

State Directory

The state directory stores Unit’s internal configuration between launches. Avoid manipulating it or relying on its contents even if tempted to do so. Instead, use only the control API to manage Unit’s configuration.

Also, the state directory should be available only to root (or the user that the main process runs as):

$ unitd -h

      ...
      --state DIRECTORY    set state directory name
                           default: "/path/to/unit/state/"

# ls -l /path/to/unit/state/

      drwx------ 2 root root 4096 ...

Configure SSL/TLS§

Rationale: To protect your client connections in production scenarios, configure SSL certificate bundles for your Unit installation.

Actions: For details, see Certificate Management and TLS with Certbot.

Error-Proof Your Routes§

Rationale: Arguably, routes are the most flexible and versatile part of the Unit configuration. Thus, they must be as clear and robust as possible to avoid loose ends and gaping holes.

Actions: Familiarize yourself with the matching logic and double-check all patterns that you use.

Details

Some considerations:

  • Mind that variables contain arbitrary user-supplied request values; variable-based pass values in listeners and routes must account for malicious requests, or the requests must be properly filtered.
  • Create matching rules to formalize the restrictions of your Unit instance and the apps it runs.
  • Configure shares only for directories and files you intend to make public.

Protect App Data§

Rationale: Unit’s architecture involves many processes that operate together during app delivery; improper process permissions can make sensitive files available across apps or even publicly.

Actions: Properly configure your app directories and shares: apps and the router process need access to them. Still, avoid loose rights such as the notorious 777, instead assigning them on a need-to-know basis.

File Permissions

To configure file permissions for your apps, check Unit’s build-time and run-time options first:

$ unitd -h

      --user USER          set non-privileged processes to run as specified user
                           default: "unit_user"

      --group GROUP        set non-privileged processes to run as specified group
                           default: user's primary group

$ ps ax | grep unitd

      ... unit: main v1.24.0 [... --user unit_user --group unit_group ...]

In particular, this is the account the router process runs as. Use this information to set up permissions for the app code or binaries and shared static files. The main idea is to limit each app to its own files and directories while simultaneously allowing Unit’s router process to access static files for all apps.

Specifically, the requirements are as follows:

  • All apps should run as different users so that the permissions can be configured properly. Even if you run a single app, it’s reasonable to create a dedicated user for added flexibility.
  • An app’s code or binaries should be reachable for the user the app runs as; the static files should be reachable for the router process. Thus, each part of an app’s directory path must have execute permissions assigned for the respective users.
  • An app’s directories should not be available to other apps or non-privileged system users. The router process should be able to access the app’s static file directories. Accordingly, the app’s directories must have read and execute permissions assigned for the respective users.
  • The files and directories that the app is designed to update should be writable only for the user the app runs as.
  • The app code should be readable (and executable in case of external apps) for the user the app runs as; the static content should be readable for the router process.

A detailed walkthrough to guide you through each requirement:

  1. If you have several independent apps, running them with a single user account poses a security risk. Consider adding a separate system user and group per each app:

    # useradd -M app_user
    # groupadd app_group
    # usermod -L app_user
    # usermod -a -G app_group app_user
    

    Even if you run a single app, this helps if you add more apps or need to decouple permissions later.

  2. It’s important to add Unit’s non-privileged user account to each app group:

    # usermod -a -G app_group unit_user
    

    Thus, Unit’s router process can access each app’s directory and serve files from each app’s shares.

  3. A frequent source of issues is the lack of permissions for directories inside a directory path needed to run the app, so check for that if in doubt. Assuming your app code is stored at /path/to/app/:

    # ls -l /
    
          drwxr-xr-x  some_user some_group  path
    
    # ls -l /path/
    
          drwxr-x---  some_user some_group  to
    

    This may be a problem because the to/ directory isn’t owned by app_user:app_group and denies all permissions to non-owners (as the --- sequence tells us), so a fix can be warranted:

    # chmod o+rx /path/to/
    

    Another solution is to add app_user to some_group (assuming this was not done before):

    # usermod -a -G some_group app_user
    
  4. Having checked the directory tree, assign ownership and permissions for your app’s directories, making them reachable for Unit and the app:

    # chown -R app_user:app_group /path/to/app/
    # chown -R app_user:app_group /path/to/static/app/files/
    # find /path/to/app/ -type d -exec chmod u=rx,g=rx,o= {} \;
    # find /path/to/static/app/files/ -type d -exec chmod u=rx,g=rx,o= {} \;
    
  5. If the app needs to update specific directories or files, make sure they’re writable for the app alone:

    # chmod u+w /path/to/writable/file/or/directory/
    

    In case of a writable directory, you may also want to prevent non-owners from messing with its files:

    # chmod +t /path/to/writable/directory/
    

    Note

    Usually, apps store and update their data outside the app code directories, but some apps may mix code and data. In such a case, assign permissions on an individual basis, making sure you understand how the app uses each file or directory: is it code, read-only content, or writable data.

  6. For embedded apps, it’s usually enough to make the app code and the static files readable:

    # find /path/to/app/code/ -type f -exec chmod u=r,g=r,o= {} \;
    # find /path/to/static/app/files/ -type f -exec chmod u=r,g=r,o= {} \;
    
  7. For external apps, additionally make the app code or binaries executable:

    # find /path/to/app/ -type f -exec chmod u=rx,g=rx,o= {} \;
    # find /path/to/static/app/files/ -type f -exec chmod u=r,g=r,o= {} \;
    
  8. To run a single app, configure Unit as follows:

    {
        "listeners": {
            "*:80": {
                "pass": "routes"
            }
        },
    
        "routes": [
            {
                "action": {
                    "share": "/path/to/static/app/files/",
                    "fallback": {
                        "pass": "applications/app"
                    }
                }
            }
        ],
    
        "applications": {
            "app": {
                "type": "...",
                "user": "app_user",
                "group": "app_group"
            }
        }
    }
    
  9. To run several apps side by side, configure them with appropriate user and group names. The following configuration distinguishes apps based on the request URI, but you can implement another scheme such as different listeners:

    {
        "listeners": {
            "*:80": {
                "pass": "routes"
            }
        },
    
        "routes": [
            {
                "match": {
                    "uri": "/app1/*"
                },
    
                "action": {
                    "share": "/path/to/static/app1/files/",
                    "fallback": {
                        "pass": "applications/app1"
                    }
                }
            },
    
            {
                "match": {
                    "uri": "/app2/*"
                },
    
                "action": {
                    "share": "/path/to/static/app2/files/",
                    "fallback": {
                        "pass": "applications/app2"
                    }
                }
            }
        ],
    
        "applications": {
            "app1": {
                "type": "...",
                "user": "app_user1",
                "group": "app_group1"
            },
    
            "app2": {
                "type": "...",
                "user": "app_user2",
                "group": "app_group2"
            }
        }
    }
    

Note

As usual with permissions, different steps may be required if you use ACLs.

App Internals

Unfortunately, quite a few web apps are built in a manner that mixes their source code, data, and configuration files with static content, which calls for complex access restrictions. The situation is further aggravated by the inevitable need for maintenance activities that may leave a footprint of extra files and directories unrelated to the app’s operation. The issue has several aspects:

  • Storage of code and data at the same locations, which usually happens by (insufficient) design. You neither want your internal data and code files to be freely downloadable nor your user-uploaded data to be executable as code, so configure your routes and apps to prevent both.
  • Exposure of configuration data. Your app-specific settings, .ini or .htaccess files, and credentials are best kept hidden from prying eyes, and your routing configuration should reflect that.
  • Presence of hidden files from versioning, backups by text editors, and other temporary files. Instead of carving your configuration around these, it’s best to keep your app free of them altogether.

If these can’t be avoided, investigate the inner workings of the app to prevent exposure, for example:

{
    "routes": {
        "app": [
            {
                "match": {
                    "uri": [
                        "*.php",
                        "*.php/*"
                    ]
                },

                "action": {
                    "pass": "applications/app/direct"
                }
            },
            {
                "match": {
                    "uri": [
                        "!/sensitive/*",
                        "!/data/*",
                        "!/app_config_values.ini",
                        "!*/.*",
                        "!*~"
                    ]
                },

                "action": {
                    "share": "/path/to/app/",
                    "fallback": {
                        "pass": "applications/app/index"
                    }
                }
            }
        ]
    }
}

However, this does not replace the need to set up file permissions; use both matching rules and per-app user permissions to manage access. For more info and real-life examples, refer to our app howtos and the ‘File Permissions’ callout above.

Unit's Process Summary

Unit’s processes are detailed elsewhere, but here’s a synopsis of the different roles they have:

ProcessPrivileged?User and GroupDescription
MainYesWhoever starts the unitd executable; by default, root.Runs as a daemon, spawning Unit’s non-privileged and app processes; requires numerous system capabilities and privileges for operation.
ControllerNoSet by --user and --group options at build or execution; by default, unit.Serves the control API, accepting reconfiguration requests, sanitizing them, and passing them to other processes for implementation.
DiscoveryNoSet by --user and --group options at build or execution; by default, unit.Discovers the language modules in the module directory at startup, then quits.
RouterNoSet by --user and --group options at build or execution; by default, unit.Serves client requests, accepting them, processing them on the spot, passing them to app processes, or proxying them further; requires access to static content paths you configure.
App processesNoSet by per-app user and group options; by default, --user and --group values.Serve client requests that are routed to apps; require access to paths and namespaces you configure for the app.

You can check all of the above on your system when Unit is running:

$ ps aux | grep unit

      ...
      root   ... unit: main v1.24.0
      unit   ... unit: controller
      unit   ... unit: router
      unit   ... unit: "foobar" application

The important outtake here is to understand that Unit’s non-privileged processes don’t require running as root. Instead, they should have the minimal privileges required to operate, which so far means the ability to open connections and access the application code and the static files shared during routing.

Prune Debug and Access Logs§

Rationale: Unit stores potentially sensitive data in its general and access logs; their size can also become a concern if debug mode is enabled.

Actions: Secure access to the logs and ensure they don’t exceed the allowed disk space.

Details

Unit can maintain two different logs:

  • A general-purpose log that is enabled by default and can be switched to debug mode for verbosity.
  • An access log that is off by default but can be enabled via the control API.

If you enable debug-mode or access logging, rotate these logs with tools such as logrotate to avoid overgrowth. A sample logrotate configuration (use real log and PID file paths):

/path/to/unit.log {
    daily
    missingok
    rotate 7
    compress
    delaycompress
    nocreate
    notifempty
    su root root
    postrotate
        if [ -f /path/to/unit.pid ]; then
            /bin/kill -SIGUSR1 `cat /path/to/unit.pid`
        fi
    endscript
}

Another issue is the logs’ accessibility. Logs are opened and updated by the main process that usually runs as root. However, to make them available for a certain consumer, you may need to enable access for a dedicated user that the consumer runs as.

Perhaps, the most straightforward way to achieve this is to assign log ownership to the consumer’s account. Suppose you have a log utility running as log_user:log_group:

$ unitd --help

      ...
      --log FILE           set log filename
                           default: "/default/path/to/unit.log"

$ ps ax | grep unitd

      ... unit: main v1.24.0 [... --log /path/to/unit.log ...]

# chown log_user:log_group /path/to/unit.log

# curl -X PUT -d '"/path/to/access.log"'  \
       --unix-socket /path/to/control.unit.sock \
       http://localhost/config/access_log

      {
          "success": "Reconfiguration done."
      }

# chown log_user:log_group /path/to/access.log

If you change the log file ownership, adjust your logrotate settings accordingly:

/path/to/unit.log {
    ...
    su log_user log_group
    ...
}

Note

As usual with permissions, different steps may be required if you use ACLs.

Use Isolation Features§

Rationale: If the underlying OS allows, Unit can provide namespace and file system root isolation features that create an additional level of separation and containment for your apps.

Actions: For details, see our two blog posts about namespace and file system isolation.