This section of the documentation contains information essential to operating an instance of the Public Key Directory reference software (server-side).
Warning
This software is not in a stable enough state to deploy anywhere yet.
Please wait until the v1.0.0 major release has been tagged.
This software is written in PHP and requires PHP 8.4 or newer. It should be safe to run on any Operating System.
The Sodium cryptography library and associated PHP extension (ext-sodium) are highly recommended. A polyfill library
(sodium_compat) is provided, but we cannot guarantee the best performance
when the Sodium extension is not available.
Three database backends are currently supported: MySQL / MariaDB, PostgreSQL, and SQLite. For best performance, we
recommend installing Redis in your deployment and configuring our software to use it. If Redis is installed locally,
make sure to follow the Redis security best practices,
such as binding to 127.0.0.1.
Our software is largely webserver agnostic. Apache, nginx, or Caddy are all okay. Use whichever you're comfortable with. We recommend nginx since it's widely used and now has native ACME support. Deploying over HTTPS is required. Use LetsEncrypt with ACME. It works great.
Tip
If you already know what "LAMP Stack" means, you can skip this section.
These configurations assume you cloned the pkd-server-php repository to /var/www/pkd-server-php.
The file paths and directories provided commands provided assume you are running on Debian or Ubuntu Linux.
Note
The example configurations below send Strict-Transport-Security
headers with the includeSubdomains and preload directives.
This prioritizes transport-layer security but could revent users from accessing a subdomain that isn't HTTPS-ready
(e.g., for an internal service).
If you're deploying in such an environment, consider omitting preload until you're certain it's safe.
File: /etc/apache2/sites-available/demo.publickey.directory.conf
<VirtualHost *:80>
ServerName demo.publickey.directory
# Redirect all HTTP to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost *:443>
ServerName demo.publickey.directory
DocumentRoot /var/www/pkd-server-php/public
# TLS Configuration
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/demo.publickey.directory/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/demo.publickey.directory/privkey.pem
# Modern TLS settings
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off
# Security Headers (applied to all responses)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "no-referrer"
Header always set Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()"
# Remove server version disclosure
Header always unset X-Powered-By
ServerTokens Prod
ServerSignature Off
<Directory /var/www/pkd-server-php/public>
Options -Indexes -FollowSymLinks
AllowOverride None
Require all granted
# Front controller rewrite
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</Directory>
# Block access to sensitive files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# PHP-FPM via proxy
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.5-fpm.sock|fcgi://localhost"
</FilesMatch>
# Logging
ErrorLog ${APACHE_LOG_DIR}/demo.publickey.directory-error.log
CustomLog ${APACHE_LOG_DIR}/demo.publickey.directory-access.log combined
</VirtualHost>Ensble these modules
sudo a2enmod ssl rewrite headers proxy_fcgi setenvif
sudo a2ensite demo.publickey.directory
sudo systemctl reload apache2Note
It's generally fine to run this with Apache mod-php, but we recommend using FPM for best results (performance, process isolation, generally better security).
File: /etc/nginx/sites-available/demo.publickey.directory
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name demo.publickey.directory;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name demo.publickey.directory;
root /var/www/pkd-server-php/public;
index index.php;
# TLS Configuration
ssl_certificate /etc/letsencrypt/live/demo.publickey.directory/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/demo.publickey.directory/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Modern TLS settings
ssl_protocols TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# ^- better mobile device battery life
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/demo.publickey.directory/chain.pem;
resolver 1.1.1.1 1.0.0.1 valid=300s;
resolver_timeout 5s;
# Note: 1.1.1.1 and 1.0.0.1 are CloudFlare. Substitute your preferred DNS provider.
# Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()" always;
# No permissions because we don't need them
# Remove server version disclosure
server_tokens off;
# Front controller
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
# PHP handling
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# Security: only execute index.php
fastcgi_split_path_info ^(.+\.php)(/.+)$;
try_files $fastcgi_script_name =404;
# Hide PHP version
fastcgi_hide_header X-Powered-By;
}
# Block access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Block access to non-public directories
location ~ ^/(vendor|config|sql|tests|fuzzing|src)/ {
deny all;
}
# Logging
access_log /var/log/nginx/demo.publickey.directory-access.log;
error_log /var/log/nginx/demo.publickey.directory-error.log;
}Enable the site:
sudo ln -s /etc/nginx/sites-available/demo.publickey.directory /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxFile: /etc/caddy/Caddyfile
demo.publickey.directory {
root * /var/www/pkd-server-php/public
# TLS is automatic with Caddy (ACME/Let's Encrypt)
# Optionally configure minimum TLS version
tls {
protocols tls1.3
}
# Security Headers
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "no-referrer"
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()"
# Remove server disclosure headers
-Server
-X-Powered-By
}
# Block access to sensitive files and directories
@blocked {
path /.*
path /vendor/*
path /config/*
path /sql/*
path /tests/*
path /fuzzing/*
path /src/*
}
respond @blocked 403
# PHP-FPM
php_fastcgi unix//run/php/php8.5-fpm.sock {
root /var/www/pkd-server-php/public
}
# Front controller: serve static files, fallback to index.php
file_server
try_files {path} {path}/ /index.php?{query}
# Logging
log {
output file /var/log/caddy/demo.publickey.directory-access.log
format json
}
}Reload Caddy to enable the site:
sudo systemctl reload caddyIf you're planning to deploy with PHP FastCGI Process Manager (PHP-FPM), here's an example configuration file:
File: /etc/php/8.5/fpm/pool.d/pkd.conf
[pkd]
user = www-data
group = www-data
listen = /run/php/php8.5-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
; Security settings
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source
php_admin_value[expose_php] = Off
php_admin_value[open_basedir] = /var/www/pkd-server-php
php_admin_value[session.cookie_httponly] = On
php_admin_value[session.cookie_secure] = On
php_admin_value[session.use_strict_mode] = On
; Logging
php_admin_value[error_log] = /var/log/php/pkd-error.log
php_flag[log_errors] = onThe first thing you should do is make a config/local subdirectory (if one doesn't already exist). Copy the PHP scripts
from config to config/local that you wish to alter.
Refer to the configuration section of the technical reference for the meaning of each file.
You run chmod on every file in the config/local/ directory to lock it down as much as possible.
chmod 0750 /var/www/pkd-server-php/config/local
chmod 0640 /var/www/pkd-server-php/config/local/*.php
chmod 0600 /var/www/pkd-server-php/config/local/*.key
chmod 0600 /var/www/pkd-server-php/config/local/*.jsonThe last line of a freshly copied config/local/database.php should look like this:
return new EasyDBCache(new PDO('sqlite:' . __DIR__ . '/sqlite.db'));This is a PHP object (an instance of EasyDB-Cache) that wraps PDO (an abstraction for multiple database drivers).
Refer to the PHP manual for configuring your database connection. Generally, you will end up with something like:
$options = []; // optional
return new EasyDBCache(new PDO('mysql:host=localhost;dbname=pkd', 'username', 'password', $options));or:
$options = []; // optional
return new EasyDBCache(new PDO('pgsql:host=localhost;dbname=pkd', 'username', 'password', $options));The closing stanza of config/local/params.php contains some core settings for your Public Key Directory instance.
return new Params(
hashAlgo: 'sha256',
otpMaxLife: 120,
actorUsername: 'pubkeydir',
hostname: 'localhost',
cacheKey: $key,
httpCacheTtl: 15,
);The hashAlgo parameter MUST NOT be changed once deployed.
The otpMaxLife parameter specifies how long One-Time Passwords are allowed to live.
The actorUsername and hostname define the actor name that accepts DMs in order to publish Protocol Messages.
The default pubkeydir is fine for most setups.
The hostname parameter should match the hostname from your virtual host.
The cacheKey is used for caching data.
The httpCacheTtl parameter is the cache lifetime for HTTP responses. You can tune this as needed.
Shorter TTLs = fresher responses for clients. Longer TTLs = less load on the HTTP API.