What is Polluxd
polluxd is Gemini file and CGI server implementation for unix-like systems. It is the reference server implementation for the libpxd Gemini protocol library. It has been tested on Linux, OpenBSD, FreeBSD, and NetBSD and has been built with both gcc and clang.
For bug reports, feature requests, or general feedback please email: ingrix at sdf dot org
(Correspondence need not be detailed)
Features
Polluxd can:
- Serve static files, pipes, unix sockets.
- Serve directory listings (aka autoindex)
- Serve CGI scripts.
- Check for client certificate existence when accessing paths.
- Check that a client certificate is allowed/whitelisted when accessing paths.
Security mitigations.
polluxd has been written with some security in mind.
The main server can be made to run from a chroot, can drop privileges, and contains a docroot specification for ensuring that files are always served from within a specific hierarchy. On OpenBSD the docroot is further enforced by the use of unveil(2).
Each location block (see below) which serves CGI scripts can be run from a chroot and set of user/group permissions. The CGI script chroot/docroot/user/group may be distinct from the main server. The CGI scripts are also served in such a way that they never have access to the server's key material at any point, which helps protect the server integrity.
Building
polluxd is built alongside the pxd library. The only other dependencies are the standard C library and OpenSSL (or a compatible library, e.g. LibreSSL). It should build on
# Extract the source and build tar -xf libpxd-d082e53-20250825.tar.gz cd libpxd-d082e53 make
To run polluxd you need to create a config file (see below) and make sure libpxd.so is in the library search path. If we assume you're running out of the build directory:
export LD_LIBRARY_PATH=$PWD ./polluxd/polluxd -f polluxd.conf
Configuration
For a quickstart, see the next section
A configuration file must be provided to polluxd to configure its behavior. The configuration is line-based and is largely a key-value specification, with the exception of the location blocks which are formatted like 'location /path {}'.
The polluxd configuration consists of two parts, top-level configuration options and location-specific configurations. Top-level options set whole-server settings and defaults for location-specific settings. Location-specific settings define the association between Gemini request paths and filesystem hierarchies, and the behaviors that are taken for those requests.
Default behavior and a simple configuration
The simplest configuration is something like this:
host=ingrix.info
listen_addr=127.0.0.1
port=1965
cert_file=cert.pem
key_file=key.pem
docroot=/var/gemini
index_file=index.gmi
location / {
action = file
}
What this gets you:
- A Gemini server that listens on 127.0.0.1:1965 and serves files from /var/gemini
- Directories with index.gmi files can be served directly
General options
The general or top-level configuration options are:
- host - the hostname for the server (e.g. ingrix.info)
- listen_addr - the IPv4, IPv6, or unix socket to listen for connections on. Only a single address may be specified as of 20250818.
- port - the port number to listen on (valid only with IPv4 and IPv6)
- cert_file - the SSL certificate for the server to use. This can be generated with the openssl command line utilities.
- key_file - the SSL private key corresponding to cert_file. This can be generated with the openssl command line utilities. See "Notes" below if you want to use a password-protected key file.
- docroot - the (default) directory that should contain all files that will be served. Note: this is relative to the chroot path if one is specified.
- index_file - the default name of the file that should be served if the client requests a directory. This is also a location-specific option. If this value is not specified (and autoindexing is not done) then a request for a directory results in a "Not found" error.
- autoindex - the default autoindexing setting. If the client requests a directory and an index_file for the location is not found (or not specified), then polluxd will auto-generate an gemtext-formatted index of the directory's contents.
- chroot_dir - the directory in the main system to chroot into before accepting requests. Every other configuration option is specified relative to this directory.
- drop_user - the username to drop permissions to before accepting requests. You can probably only do this when running as root.
- drop_group - the groupname to drop permissions to before accepting requests.
- logfile - where to write log output. Note: the same thing can be accomplished by redirecting stderr.
Location-specific options
TL;DR locations are specified in blocks. Basic wildcards can be used which match single path components.
location / {
...options...
}
location /cgi {
...options...
}
location /~* {
...options for serving home dirs...
}
The paths specified in the 'location' line represent the client's requested path (i.e. what is received via Gemini). Directories are treated hierarchically, and the closest/deepest location block will be used for a given request path. Sub-directories override the settings of their parent directories (e.g. you can use a 'location /some/path' to override settings for the 'location /some'). The root directory "/" is not treated specially, and must be specified as a location block if you want to serve anything from the top-level docroot directory.
Basic wildcards can be used for individual path components (see fnmatch(3)). These wildcards will match only single components. Example:
location /some/~*/path {
...
}
# the above block will match requests for /some/~ingrix/path and /some/~/path but not /some/~ingrix/sub/path
The list of location-specific configuration options is:
- action - what to do with data from this location. Valid values are "deny" "file" or "cgi". See "Actions" below for details.
- cgi_user - if action is "cgi", this is the user that will run the CGI scripts for the location hierarchy.
- cgi_group - like cgi_user, but the group that will be used.
- cgi_chroot - the chroot that the CGI requests will run from. This may be different from the top-level chroot_dir, but will wall back to the chroot_dir value if not specified.
- cgi_docroot - the directory that scripts will be searched for CGI script requests (if not using cgi_script below)
- cgi_script - a fixed script to execute (relative to cgi_chroot) that should be executed for all requests.
- strip - the number of path components that should be stripped during path translation (see "Path Translation" below).
- prefix - the path that should be prefixed to the stripped request path during path translation (see "Path Translation" below)
- require_cert - require a cryptographically-valid client certificate for viewing the hierarchy. See the section below on Requiring certificates/Certificate whitelists.
- allowed_cert_file - a whitelist of allowable certificates. This implies require_cert. See the section below on Requiring certificates/Certificate whitelists.
- index_file - the name of the file to serve if a directory is requested. This is a location-specific version of the same top-level option. This will override the top-level specification.
- autoindex - whether to generate a directory index location-specific version of the same top-level option. This will override the top-level specification.
Actions
The available actions for each location block are "deny", "file", and "cgi"
"deny" returns a "Permission denied" error to the client. This is the default setting for all hierarchies.
"file" serves a static file from the directory hierarchy, or reads from a pipe or socket.
"cgi" executes a CGI script as translated from the request path (or a fixed script if cgi_script is specified)
Path translation
TL;DR:
# Assume the configuration settings: # docroot = /tmp/docroot # prefix = subdir # strip = 1 # Gemini request: gemini://ingrix.info/some/random/extra/../path (request path) /some/random/extra/../path | | canonicalize v /some/random/path | | strip v /random/path | | prefix v /subdir/random/path | | docroot v /tmp/docroot/subdir/random/path (the filesystem path)
Polluxd translates paths from Gemini request path to an effective filesystem path using the 'docroot', 'strip', and 'prefix' options appropriate for each location block. The procedure is as follows:
- The request path is translated to canonical form by converting percent-encoded characters to their raw equivalent (NUL bytes are stripped), '.' and '..' entries are evaluated out from the decoded path, and the path is made absolute (i.e. prefixed with '/').
- If 'strip' is greater than 0, the leading 'strip' components of the path are removed. Example: strip=1, path=/some/random/path will be translated to /random/path (one leading component removed).
- The 'prefix' setting is prepended to the stripped path. Example: prefix=/subdir/ and path=/random/path will result in /subdir/random/path.
- The stripped and prefixed path is further prefixed with the value of 'docroot'. Example: docroot=/tmp, path=/subdir/random/path will result in /tmp/subdir/random/path.
Requiring certificates/Certificate whitelists
Each location block can contain the 'require_cert' or 'allowed_cert_file' options. If one or both are specified and the certificate validation fails then a 'Permission denied' error is returned for the hierarchy.
'require_cert' means that the client must send a cryptographically valid client certificate to the server. The specific information in the certificate doesn't matter, but one has to be present. This is somewhat useful for filtering out bots from the Gemini traffic.
'allowed_cert_file' specifies a path (relative to the chroot) to read valid certificate hashes from. The file is a simple list of SHA256 hashes of allowed client certificates, one certificate per line. The file also supports #-prefixed comments at the ends of lines to provide comments. Whitespace is also stripped off.
Notes and Tricks
Password-protected keys
If you want to use a password-protected SSL keyfile, you can direct this key_file option to a fifo on the filesystem, decrypt the key, and pipe the decrypted key data to the fifo. You can also do this for cert_file but honestly I don't know why you would want to.