NGINX Unit

安全检查清单§

Unit 的核心将安全性作为其首要任务之一;我们的开发遵循适当的最佳实践,专注于使代码健壮可靠。然而,即使是最强化的系统也需要适当的设置、配置和维护。

本指南列出了从安装到各个应用配置保护 Unit 的步骤。

定期更新 Unit§

基本原理:每个版本都会引入错误修复和新功能,以提高安装的安全性。

操作:关注我们最新的新闻,并在新版本发布后不久升级到新版本。

详细信息

具体的升级步骤取决于安装方法

  • 推荐使用我们的官方软件包或 Docker 镜像;使用这些软件包或镜像,只需使用您选择的软件包管理器更新unit-*软件包或切换到较新的镜像即可。

  • 如果您使用第三方安装方法,请查阅维护人员的文档以了解详细信息。

  • 如果您从源文件安装 Unit,请从头开始重新构建并重新安装 Unit 及其模块。

安全套接字和状态§

基本原理:您的控制套接字和状态目录提供了对 Unit 配置的无限制访问,这需要严格保护。

操作:我们官方软件包中的默认配置通常足够;如果您使用其他安装方法,请确保控制套接字和状态目录是安全的。

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:/default/path/to/control.unit.sock"

$ ps ax | grep unitd

      ... unit: main v1.32.1 [... --control /path/to/control.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 203.0.113.14:8080
$ curl 203.0.113.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 203.0.113.14:8080

    curl: (7) Failed to connect to 203.0.113.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: "/default/path/to/unit/state/"

$ ps ax | grep unitd

      ... unit: main v1.32.1 [... --state /path/to/unit/state/ ...]

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

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

配置 SSL/TLS§

基本原理:为保护您的客户端连接在生产场景中,为您的 Unit 安装配置 SSL 证书包。

操作:有关详细信息,请参阅SSL/TLS 证书TLS with Certbot

对您的路由进行错误验证§

基本原理:可以说,路由是 Unit 配置中最灵活且用途最广泛的部分。因此,它们必须尽可能清晰且健壮,以避免出现漏洞和巨大缺陷。

操作:熟悉匹配逻辑并仔细检查您使用的所有模式

详细信息

一些注意事项

  • 请注意,变量包含任意用户提供的请求值;基于变量的pass值在侦听器路由中必须考虑恶意请求,或者必须正确过滤请求。

  • 创建匹配规则以规范您的 Unit 实例及其运行的应用程序的限制。

  • 仅为要公开的目录和文件配置共享

保护应用程序数据§

基本原理:Unit 的架构涉及在应用程序交付期间一起操作的许多进程;不当的进程权限可能会使敏感文件跨应用程序甚至公开可用。

操作:正确配置您的应用程序目录和共享:应用程序和路由器进程需要访问它们。不过,避免使用臭名昭著的777等宽松权限,而应根据需要分配权限。

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.32.1 [... --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/$uri",
                    "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/$uri",
                    "fallback": {
                        "pass": "applications/app1"
                    }
                }
            },
    
            {
                "match": {
                    "uri": "/app2/*"
                },
    
                "action": {
                    "share": "/path/to/static/app2/files/$uri",
                    "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/static$uri",
                    "types": [
                        "image/*",
                        "text/*",
                        "application/javascript"
                    ],

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

Process

Privileged?

User and Group

Description

Main

Yes

Whoever 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.

Controller

No

Set 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.

Discovery

No

Set by --user and --group options at build or execution; by default, unit.

Discovers the language modules in the module directory at startup, then quits.

Router

No

Set 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 processes

No

Set 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.32.1
      unit   ... unit: controller
      unit   ... unit: router
      unit   ... unit: "front" 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.

精简调试和访问日志§

基本原理:单元在常规日志和访问日志中存储潜在的敏感数据;如果启用调试模式,其大小也可能成为问题。

操作:保护对日志的访问,确保它们不超过允许的磁盘空间。

详细信息

单元可以维护两个不同的日志

  • 默认情况下启用的通用日志,可以切换到调试模式以提高详细程度。

  • 默认情况下关闭的访问日志,可以通过控制 API 启用。

如果您启用调试模式或访问日志记录,请使用 logrotate 等工具轮换这些日志,以避免过度增长。一个示例 logrotate 配置

/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
}

找出日志和 PID 文件路径

$ unitd -h

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

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

$ ps ax | grep unitd

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

另一个问题是日志的可访问性。日志由 主进程 打开和更新,该进程通常以 root 身份运行。但是,为了让特定使用者可以使用它们,您可能需要为使用者运行的专用用户启用访问权限。

也许,实现此目的最直接的方法是将日志所有权分配给使用者的帐户。假设您有一个以 log_user:log_group 身份运行的日志实用程序

# 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

如果您更改日志文件所有权,请相应地调整 logrotate 设置

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

注意

与权限一样,如果您使用 ACL,则可能需要不同的步骤。

添加限制、隔离§

基本原理:如果底层操作系统允许,单元提供一些功能,为您的应用创建额外的隔离和包含级别,例如

操作:有关更多详细信息,请参阅我们关于 路径限制命名空间文件系统 隔离的博客文章。