Skip to content

Commit

Permalink
Add support for openbsd.pledge_promises, openbsd.pledge_execpromises …
Browse files Browse the repository at this point in the history
…and openbsd.unveil ini directives
  • Loading branch information
tvlooy committed Nov 22, 2023
1 parent d60b1d8 commit 03245c9
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 89 deletions.
289 changes: 224 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,180 @@

This is a PHP extension that adds support for OpenBSD's pledge and unveil system calls.

## OpenBSD Ports
## Requirements

This extension works with >= PHP 7.2 and needs at least OpenBSD 6.4.

## The theory

The [pledge(2) system call](http://man.openbsd.org/OpenBSD-current/man2/pledge.2) allows a program to restrict the types
of operations the program can do after that point. Unlike other similar systems, pledge is specifically designed for
programs that need to use a wide variety of operations on initialisation, but a fewer number after initialisation (when
user input will be accepted).

This package is present in OpenBSD ports. To install it, make sure you [have the ports tree](https://www.openbsd.org/faq/ports/ports.html#PortsFetch)
on your machine. Next, install the port:
The pledge system call is supported on OpenBSD >= 5.9

The first call to [unveil(2) system call](http://man.openbsd.org/OpenBSD-current/man2/unveil.2) restricts the filesystem
view. Subsequent calls can open it more. To prevent further unveiling, call unveil with no parameters or drop the unveil
pledge if the program is pledged.

The unveil system call is supported on OpenBSD >= 6.4

## OpenBSD installation

This package is present in OpenBSD packages. To install it, run:

```sh
pkg_add pecl82-pledge-2.1.2
```

This package can also be installed from ports. To install it, make sure you
[have the ports tree](https://www.openbsd.org/faq/ports/ports.html#PortsFetch) on your machine. Next, install the port:

```sh
cd /usr/ports/www/pecl-pledge/
env FLAVOR=php80 make FETCH_PACKAGES= install
env FLAVOR=php82 make FETCH_PACKAGES= install
```

Using the extension:

```
echo 'extension=pledge' > /etc/php-8.0/pledge.ini
php-8.0 -m | grep pledge
```sh
echo 'extension=pledge' > /etc/php-8.2/pledge.ini
php-8.2 -m | grep pledge
```

## Requirements
You can then use the `pledge()` and `unveil()` functions, or set a pledge and unveil default with ini values:

This extension works with >= 7.2 and needs at least OpenBSD 6.4. Note that with PHP >= 7.4 this extension is more or less useless but still works.
You can use the Foreign Function Interface (FFI) extension that is in core to call libc functions:
- openbsd.pledge_promises
- openbsd.pledge_execpromises
- openbsd.unveil

```bash
$ cat test_ffi.php
<?php
Promises are the pledge promises for your main process, execpromises are for child processes (or the same as the parent
if not specified). The openbsd.unveil ini setting can receive many unveil calls eg:
/html/public:r,/var/www/html/tmp:rw would make the /var/www/html/public directory readable and the
/var/www/html/tmp readable and writeable. If you want to restrict further calls to unveil, end with null:null.

$libc = FFI::cdef('
int unveil(const char *path, const char *permissions);
', 'libc.so.92.5');
## FPM configuration

var_dump(count(scandir('/etc')));
$libc->unveil(__DIR__, 'r');
scandir('/etc');
You can set promises and unveils in your PHP-FPM config. Eg:

$ php -dextension=ffi test_ffi.php
int(77)
```
php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil inet
php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc,null:null
```

Warning: scandir(/etc): failed to open dir: No such file or directory in /home/tvl/test_ffi.php on line 9
An simplified httpd example config:

Warning: scandir(): (errno 2): No such file or directory in /home/tvl/test_ffi.php on line 9
```
chroot "/var/www"
server "example.com" {
listen on * port 80
root "/htdocs/public"
directory index "index.php"
# Assets not served by PHP
location match "\.(css|gif|jpg|png|js)$" {
pass
}
location match "/specific-path-1" {
request rewrite "/index.php/%1"
fastcgi socket "/run/php-fpm-specific-path-1.sock"
}
location match "/specific-path-2" {
request rewrite "/index.php/%1"
fastcgi socket "/run/php-fpm-specific-path-2.sock"
}
# The default PHP handler
location match "^/(.+)$" {
request rewrite "/index.php/%1"
fastcgi socket "/run/php-fpm.sock"
}
}
```

## The theory
With a simplified PHP-FPM config:

The [pledge(2) system call](http://man.openbsd.org/OpenBSD-current/man2/pledge.2) allows a program to restrict the types
of operations the program can do after that point. Unlike other similar systems, pledge is specifically designed for
programs that need to use a wide variety of operations on initialisation, but a fewer number after initialisation (when
user input will be accepted).
```
[global]
include=/etc/php-fpm.d/*.conf
[specific-path-1]
user = www
group = www
listen.owner = www
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chroot = /var/www
pm.max_requests = 1
listen = /var/www/run/php-fpm-specific-path-1.sock
php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil
php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc
[specific-path-2]
user = www
group = www
listen.owner = www
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chroot = /var/www
pm.max_requests = 1
listen = /var/www/run/php-fpm-specific-path-2.sock
php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil
php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc
[www]
user = www
group = www
listen.owner = www
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chroot = /var/www
pm.max_requests = 1
listen = /var/www/run/php-fpm.sock
php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil inet
php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc
```

The pledge system call is supported on OpenBSD >= 5.9
Don't forget to call `unveil(null, null);` in your PHP userland to disallow future unveil calls, or specify null:null as
the last argument eg:

The first call to [unveil(2) system call](http://man.openbsd.org/OpenBSD-current/man2/unveil.2) restricts the filesystem
view. Subsequent calls can open it more. To prevent further unveiling, call unveil with no parameters or drop the unveil
pledge if the program is pledged.

The unveil system call is supported on OpenBSD >= 6.4
```
php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc,null:null
```

## Build
## Build from source

Install a release with PECL (see [security/pledge](https://pecl.php.net/package/pledge)) or build the latest from source:
Install a release from PECL [security/pledge](https://pecl.php.net/package/pledge) or build the latest from source:

```
```sh
git clone https://github.com/tvlooy/php-pledge
cd php-pledge
phpize
Expand All @@ -75,19 +187,17 @@ doas make install
Add the following to your configuration to enable the extension:

```
extension=pledge.so
extension=pledge
```

Run the tests with:

```
```sh
NO_INTERACTION=1 make test
```

## Pledge usage

Note: You will need to include the ```stdio``` promise every time because PHP will not work well without it.

All promises are documented in [the OpenBSD pledge(2) manual page](http://man.openbsd.org/OpenBSD-current/man2/pledge.2).

When pledging your program keep an eye on ```/var/log/messages``` to see your violations.
Expand All @@ -106,6 +216,25 @@ pledge('stdio inet unix dns');

If the PHP ```pledge()``` call fails, it will throw a ```\PledgeException```.

The ```error``` pledge will make the call fail but will not kill the program. An example:

```sh
$ php \
-dopenbsd.pledge_promises='stdio dns' \
-r 'echo gethostbyname("openbsd.org");'
199.185.178.80

$ php \
-dopenbsd.pledge_promises='stdio error' \
-r 'echo gethostbyname("openbsd.org");'
openbsd.org

$ php \
-dopenbsd.pledge_promises='stdio' \
-r 'echo gethostbyname("openbsd.org");'
Abort trap (core dumped)
```

To set promises for an execve child:

```php
Expand All @@ -132,52 +261,76 @@ unveil();

# No longer works
unveil('/var', 'r');

```

If the PHP ```unveil()``` call fails, it will throw a ```\UnveilException```.

You can also restrict a process to /var/empty with unveil from the CLI:

```sh
$ php \
-dopenbsd.unveil='/var/empty:r,null:null' \
-dopenbsd.pledge_promises='stdio dns' \
-r 'echo gethostbyname("openbsd.org");'
199.185.178.80
```

## Usage tips

Be careful what you pledge! If you run PHP with mod_php, you will be pledging an Apache child processes! If you pledge
php_fpm you will be pledging it for the lifetime of the process, not just the request!
See [ctors/pledge-symfony-routing](https://packagist.org/packages/ctors/pledge-symfony-routing) for Symfony framework
support.

Be careful where you pledge / unveil! If you run PHP with mod_php, you will be pledging an Apache child processes! If
you pledge / unveil php_fpm you will be pledging it for the lifetime of the process, not just the request!

Set `pm.max_requests = 1` in the PHP-FPM config to always start with a new process.

For your web SAPI, make sure you limit the amount of loaded extensions. If you only need ```phar```, ```pcntl```, ...
in your CLI, then don't load them in your web SAPI.

Unveil can for example be applied to FPM processes in addition to or as an alternative to chroot.
Unveil can for example be applied to FPM processes in addition to or as an alternative to chroot. Note that PHP-FPM runs
chroot-ed by default on OpenBSD, but you can't pledge chroot, so you can't start FPM with a openbsd.pledge_promises.

Ways to configure:

Pledging CLI processes is the most convenient usecase. Examples:
- set `openbsd.pledge_promises` and/or `openbsd.unveil` in `php.ini`
- or set `php_admin_value[openbsd.pledge_promises]` and/or `php_admin_value[openbsd.unveil]` in the PHP-FPM config
- and/or apply `pledge()` and `unveil()` in your userland code

If you are running the php interactive shell with ```php -a``` you need these promises:
If you want to reuse pledged / unveiled processes, you can work around failing calls by checking if the process is
already running with a restricted view. Eg:

```php
pledge('stdio rpath wpath cpath tty ioctl');
if (is_dir('/etc')) {
unveil(__DIR__, 'r');
}
pledge('stdio rpath flock inet');
```

If you want to pledge Drupal8 or Symfony2 running on FPM, you need at least:
## A word about FFI

```php
pledge('stdio rpath wpath cpath inet dns flock fattr');
```
Note that with PHP >= 7.4 you could also use the Foreign Function Interface (FFI) extension that is in core to call libc
functions. But the FFI extension is not built by default in the OpenBSD ports/packages. And, it does not come with ini
settings like this pledge extension, so you cannot hook pledge() and unveil() into the PHP engine startup.

Preventing filesystem or network access with pledge seems impossible.
```sh
$ cat test_ffi.php
<?php

Just to serve a ```phpinfo()``` or "hello world" from FPM you need:
$libc = FFI::cdef('
int unveil(const char *path, const char *permissions);
', 'libc.so.97');

```php
pledge('stdio rpath flock inet');
```
var_dump(count(scandir('/etc')));
$libc->unveil(__DIR__, 'r');
scandir('/etc');

$ php -dextension=ffi test_ffi.php
int(77)

You can then further limit filesystem access with unveil(). If you are in web SAPI, remember that you are not limiting
filesystem access for one request but for all subsequent requests. Avoid having to add the ```unveil``` promise by checking
if the process is already running with a restricted view. Eg:
Warning: scandir(/etc): failed to open dir: No such file or directory in /home/tvl/test_ffi.php on line 9

```php
if (is_dir('/etc')) {
unveil(__DIR__, 'r');
}
pledge('stdio rpath flock inet');
Warning: scandir(): (errno 2): No such file or directory in /home/tvl/test_ffi.php on line 9
```

### Limiting network calls
Expand All @@ -189,3 +342,9 @@ block out proto {tcp udp} user your_fpm_user
pass out proto tcp to $mysql_db port 3306 user your_fpm_user
pass out proto tcp to $some_rest_api port 443 user your_fpm_user
```

### Package

Run `pecl package` to package this extension.

On OpenBSD, get a base64 sha256 checksum with `sha256 -b *.tgz` and the file size with `ls -l *.tgz`.
Loading

0 comments on commit 03245c9

Please sign in to comment.