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. Specific upgrade steps depend on your installation method:Details
unit-*
packages with your package manager of choice or
switching to a newer image.
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. If you use a Unix
control socket, ensure it is available to Unix domain sockets aren’t network accessible; for remote access, use
NGINX or a solution such as SSH: 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: Instead, opt for the loopback address to ensure all access is local to your
server: 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. 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 Control Socket
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.26.1 [... --control /path/to/control.sock ...]
# ls -l /path/to/control.unit.sock
srw------- 1 root root 0 ... /path/to/control.unit.sock
$ ssh -N -L ./here.sock:/path/to/control.unit.sock root@unit.example.com &
$ curl --unix-socket ./here.sock
{
"certificates": {},
"config": {
"listeners": {},
"applications": {}
}
}
# unitd --control 203.0.113.14:8080
$ curl 203.0.113.14:8080
{
"certificates": {},
"config": {
"listeners": {},
"applications": {}
}
}
# 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
State Directory
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.26.1 [... --state /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. Some considerations:Details
pass
values in
listeners and routes must account for malicious requests, or the
requests must be properly filtered.
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 To configure file permissions for your apps, check Unit’s build-time and
run-time options first: 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: A detailed walkthrough to guide you through each requirement: 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: Even if you run a single app, this helps if you add more apps or need to
decouple permissions later. It’s important to add Unit’s non-privileged user account to each app
group: Thus, Unit’s router process can access each app’s directory and serve
files from each app’s shares. 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 This may be a problem because the Another solution is to add Having checked the directory tree, assign ownership and permissions for
your app’s directories, making them reachable for Unit and the app: If the app needs to update specific directories or files, make sure
they’re writable for the app alone: In case of a writable directory, you may also want to prevent non-owners
from messing with its files: 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. For embedded apps, it’s usually enough to make the
app code and the static files readable: For external apps, additionally make the app code or
binaries executable: To run a single app, configure Unit as follows: 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: Note As usual with permissions, different steps may be required if you use
ACLs. 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: If these can’t be avoided, investigate the inner workings of the app to
prevent exposure, for example: 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 processes are detailed elsewhere, but here’s a
synopsis of the different roles they have: You can check all of the above on your system when Unit is running: The important outtake here is to understand that Unit’s non-privileged
processes don’t require running as 777
, instead assigning them on a need-to-know basis.File Permissions
$ 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.26.1 [... --user unit_user --group unit_group ...]
# useradd -M app_user
# groupadd app_group
# usermod -L app_user
# usermod -a -G app_group app_user
# usermod -a -G app_group unit_user
/path/to/app/
:# ls -l /
drwxr-xr-x some_user some_group path
# ls -l /path/
drwxr-x--- some_user some_group to
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/
app_user
to some_group
(assuming this was not done before):# usermod -a -G some_group app_user
# 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= {} \;
# chmod u+w /path/to/writable/file/or/directory/
# chmod +t /path/to/writable/directory/
# 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= {} \;
# 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= {} \;
{
"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"
}
}
}
{
"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"
}
}
}
App Internals
.ini
or .htaccess
files, and credentials are best kept hidden from
prying eyes, and your routing configuration should reflect that.{
"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"
}
}
}
]
}
}
Unit's Process Summary
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. $ ps aux | grep unit
...
root ... unit: main v1.26.1
unit ... unit: controller
unit ... unit: router
unit ... unit: "foobar" application
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. Unit can maintain two different logs: If you enable debug-mode or access logging, rotate these logs with tools
such as logrotate to avoid overgrowth. A sample
logrotate configuration: To figure out the log and PID file paths: Another issue is the logs’ accessibility. Logs are opened and updated by
the main process that usually 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 If you change the log file ownership, adjust your logrotate
settings accordingly: Note As usual with permissions, different steps may be required if you use
ACLs.Details
/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
}
$ 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.26.1 [... --pid /path/to/unit.pid --log /path/to/unit.log ...]
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.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
/path/to/unit.log {
...
su log_user log_group
...
}
Add Restrictions, Isolation§
Rationale: If the underlying OS allows, Unit provides features that create an additional level of separation and containment for your apps, such as:
- Share path restrictions
- Namespace and file system root isolation
Actions: For more details, see our blog posts on path restrictions, namespace and file system isolation.