OpenBSD Handbook

    • Part I. Install & Configure
      • Introduction
      • Installing OpenBSD
      • The X Window System
      • Networking
      • System Configuration
      • OpenBSD Basics
      • Managing Software: Packages and Ports
    • Part II. Daily Operations
      • Graphical Environments
      • Multimedia
      • Printing
      • Linux Compatibility
      • Windows Compatibility
      • Games
    • Part III. System Administration
      • Security
      • Virtualization
      • Storage and File Systems
      • Updating and Upgrading
      • Localization
      • The OpenBSD Boot Process
    • Part IV. Networking & Daemons
      • Services
        • Database
          • MariaDB
          • PostgreSQL
          • Redis
          • memcached
        • Directory
          • YP (NIS)
          • LDAP
        • File
          • NFS
          • Samba
        • FTP Services
          • ftpd
          • ProFTPD
          • vsftpd
          • TFTP
        • Mail
          • Dovecot
          • smtpd
          • Postfix
          • Exim
          • Rspamd
        • Name
          • Named
          • Unbound
          • NSD
        • Networking
          • OpenBGPD
          • rtadvd
          • DHCP
          • slaacd
        • Web
          • Apache
          • nginx
          • httpd
          • relayd
        • Logging
          • syslogd
        • Monitoring
          • SNMP
        • Remote Access
          • Audit OpenSSH
          • sshd
        • File Synchronization
          • rsync
        • Messaging
          • RabbitMQ
        • Time
          • NTP
      • PF
        • pfctl cheat sheet
        • PF Anchors
        • PF Filter Rules
        • PF Forwarding
        • PF Lists and Macros
        • PF Load Balancing
        • PF Logging
        • PF NAT
        • PF Options
        • PF Policies
        • PF Shortcuts
        • PF Tables
      • Advanced Networking
        • High Availability and State Replication
        • Multi-WAN and Policy-Based Routing
        • VPN and Cryptographic Tunneling
        • Classic and Lightweight Tunnels
        • IPv6 at Scale
        • QoS and Traffic Shaping
        • MPLS and Label Distribution
        • Network Services at Scale
        • Virtualization and Host Networking
        • Large-Scale L2 and L3 Design
        • Telemetry, Logging, and Flow Export
        • Hardening and Operational Safety
        • Reference Architectures
        • Troubleshooting Playbooks
      • Serial Communication
    • Part V. Miscellaneous
      • Virtualization Cheat Sheet
      • OpenBSD Cheatsheet
      • Howto
        • Install Z shell (zsh)
        • Set Up WordPress
        • Build a Simple Router and Firewall
      • OpenBSD for Linux Users
      • OpenBSD for FreeBSD Users
      • OpenBSD for macOS Users
    • Package Search
      httpd
      • Synopsis
        • Web Server History on OpenBSD
      • Web Server Comparison
      • Configuration
      • Logging
      • TLS with acme-client(1)
        • Prerequisites
        • Expose the ACME challenge
        • Configure acme-client(1)
        • Obtain the initial certificate
        • Enable HTTPS and redirect HTTP
        • Automated renewal
      • OCSP Stapling
      • Common Configuration Examples
        • 301 redirects (permanent)
        • URL prefix mapping and clean paths
        • HTTP Basic authentication
        • Static gzip delivery (gzip-static)
        • Custom error responses
      • FastCGI and Dynamic Content
        • Using slowcgi(8) (base)
        • Using PHP via php-fpm (packages)
      • Security Notes
      • Service Management and Troubleshooting

      httpd

      Synopsis #

      httpd(8) is OpenBSD’s native web server daemon. It is simple and security-focused, runs in a chroot(2) to /var/www by default, serves static content, and dispatches dynamic requests to FastCGI backends. TLS is built in and integrates with acme-client(1) for automated certificate management.

      Web Server History on OpenBSD #

      OpenBSD initially shipped with Apache 1.3, transitioned briefly to nginx (OpenBSD 5.6–5.8), and since OpenBSD 5.9 has included the current, native httpd(8) in base.

      Web Server Comparison #

      Featurehttpd(8) (base)nginx (pkg)Apache (pkg)
      Included in baseYesNoNo
      Configuration styleSimple, declarativeModular, declarativeModular, verbose
      Dynamic contentFastCGIYesYes
      TLS supportNativeYesYes
      HTTP/2 supportNoYesYes
      .htaccessNoNoYes
      Reverse proxyWith relayd(8)YesYes
      Recommended useStatic/FastCGI hostingReverse proxy, TLS offloadLegacy/complex deployments

      Configuration #

      The configuration file is httpd.conf(5) . Paths in server and location blocks are relative to the /var/www chroot unless noted otherwise.

      /etc/httpd.conf
      

      Minimal configuration to serve static content:

      types { include "/usr/share/misc/mime.types" }
      
      server "default" {
          listen on * port 80
          root "/htdocs"
      }
      

      Create the document root and a test page:

      # mkdir -p /var/www/htdocs
      # printf 'Welcome to OpenBSD httpd\n' > /var/www/htdocs/index.html
      

      Enable and start the service with rcctl(8) :

      # rcctl enable httpd
      # rcctl start httpd
      

      Validate configuration at any time:

      # httpd -n
      

      Logging #

      By default, logs are written inside the chroot:

      /var/www/logs/access.log
      /var/www/logs/error.log
      

      Follow requests in real time:

      # tail -f /var/www/logs/access.log
      

      Per-server logging can be customized:

      server "example.org" {
          log style combined
          access log "example-access.log"
          error  log "example-error.log"
          root "/htdocs/example"
          listen on * port 80
      }
      

      TLS with acme-client(1) #

      Issue and renew TLS certificates from Let’s Encrypt with acme-client(1) ; the procedure below provides a complete, current workflow.

      Prerequisites #

      • DNS A/AAAA records resolve to the server.
      • TCP ports 80 and 443 are reachable (adjust pf(4) ).
      • The ACME “http-01” challenge path /.well-known/acme-challenge/ is exposed.

      Create the standard challenge directory inside the chroot:

      # install -d -o root -g www -m 0755 /var/www/acme
      

      Expose the ACME challenge #

      Map the challenge path to /var/www/acme:

      server "example.org" {
          listen on * port 80
          root "/htdocs/example.org"
      
          # ACME http-01 challenge
          location "/.well-known/acme-challenge/*" {
              root "/acme"
              request strip 2
              directory no auto index
          }
      }
      

      Reload:

      # rcctl reload httpd
      

      Configure acme-client(1) #

      Create /etc/acme-client.conf (see acme-client.conf(5) ):

      authority letsencrypt {
          api url "https://acme-v02.api.letsencrypt.org/directory"
          account key "/etc/acme/letsencrypt-privkey.pem"
      }
      
      domain "example.org" {
          alternative names { "www.example.org" }
          domain key "/etc/ssl/private/example.org.key"
          domain full chain certificate "/etc/ssl/example.org.fullchain.pem"
          sign with letsencrypt
          challengedir "/var/www/acme"
      }
      

      Private keys under /etc/ssl/private/ must be readable by root only.

      Obtain the initial certificate #

      # acme-client -v example.org && rcctl reload httpd
      

      On success, the certificate chain is /etc/ssl/example.org.fullchain.pem and the private key is /etc/ssl/private/example.org.key.

      Enable HTTPS and redirect HTTP #

      Keep port 80 for ACME challenges and 301 redirects; serve the site on port 443 with HSTS.

      types { include "/usr/share/misc/mime.types" }
      
      # HTTP: serve ACME challenges; redirect everything else
      server "example.org" {
          listen on * port 80
          root "/htdocs/example.org"
      
          location "/.well-known/acme-challenge/*" {
              root "/acme"
              request strip 2
              directory no auto index
          }
      
          location * {
              block return 301 "https://$HTTP_HOST$REQUEST_URI"
          }
      }
      
      # HTTPS: primary site
      server "example.org" {
          listen on * tls port 443
          hsts
      
          tls {
              certificate "/etc/ssl/example.org.fullchain.pem"
              key         "/etc/ssl/private/example.org.key"
              # ocsp       "/etc/ssl/example.org.ocsp"   # see OCSP section
          }
      
          root "/htdocs/example.org"
          directory index "index.html"
      }
      

      Automated renewal #

      Schedule regular renewals and reload only on change; ~ randomizes the minute (see crontab(5) ).

      ~ 3 * * * acme-client example.org && rcctl reload httpd
      

      OCSP Stapling #

      Online Certificate Status Protocol (OCSP) provides certificate revocation status. OCSP stapling allows the server to fetch a signed OCSP response from the certificate authority and include (“staple”) it in the TLS handshake, improving privacy and reducing latency.

      Generate and maintain a stapled OCSP response with ocspcheck(8) , then reference it in the tls {} block. Paths for tls ocsp are absolute, not relative to the chroot.

      # ocspcheck -o /etc/ssl/example.org.ocsp /etc/ssl/example.org.fullchain.pem
      # rcctl reload httpd
      

      Refresh periodically via cron(8) ) because OCSP responses expire more frequently than certificates:

      30 2 * * * ocspcheck -q -o /etc/ssl/example.org.ocsp /etc/ssl/example.org.fullchain.pem && rcctl reload httpd
      

      If the stapling file is missing or expired, TLS continues to operate without stapling; clients may fall back to live OCSP queries.

      Common Configuration Examples #

      All directives are documented in httpd.conf(5) .

      301 redirects (permanent) #

      HTTP→HTTPS (shown above) and common host canonicalization:

      # Redirect www to apex
      server "www.example.org" {
          listen on * port 80
          listen on * tls port 443
          tls { certificate "/etc/ssl/example.org.fullchain.pem"
                key         "/etc/ssl/private/example.org.key" }
          block return 301 "https://example.org$REQUEST_URI"
      }
      

      Directory relocation:

      location "/old-section/*" {
          block return 301 "/new-section/$REQUEST_URI"
      }
      

      URL prefix mapping and clean paths #

      Use request strip to remove leading components before file lookup.

      server "example.org" {
          listen on * tls port 443
          root "/htdocs"
      
          # Serve /app/* from /var/www/htdocs/app
          location "/app/*" {
              root "/htdocs/app"
              request strip 1
              directory index "index.html"
          }
      }
      

      Single-page application fallback:

      server "spa.example.org" {
          listen on * tls port 443
          root "/htdocs/spa"
          directory index "index.html"
      
          # Known asset types are served as-is
          location "/*.{css,js,png,jpg,svg,ico}" { }
      
          # Everything else falls back to index.html
          location * {
              block return 200 "/index.html"
          }
      }
      

      HTTP Basic authentication #

      OpenBSD’s httpd supports HTTP Basic Authentication for restricting access to specific locations. Two supported configuration styles are available.

      Which method should be used? #

      • Use an htpasswd file (authenticate ... with)
        Recommended for production deployments. Credentials are stored in a separate file, allowing user accounts to be added or removed without modifying httpd.conf.

      • Use an inline authentication {} block
        Appropriate for small installations, laboratory environments, or self-contained configurations where consolidating settings within a single file is desirable.

      Both methods rely on bcrypt hashes for secure password storage.

      Method 1 — Use an htpasswd file (recommended) #

      This approach separates credential management from the primary configuration file and simplifies long-term administration.

      1. Create a password file

      Create a directory within the default chroot and generate bcrypt password hashes using htpasswd(1):

      $ doas mkdir -p /var/www/auth
      $ doas htpasswd -c /var/www/auth/mysite alice
      $ doas htpasswd /var/www/auth/mysite bob
      $ doas chown www: /var/www/auth/mysite
      

      The path specified in httpd.conf must be relative to /var/www (the default chroot environment).

      1. Reference it in httpd.conf
      server "example.org" {
          listen on * tls port 443
          root "/htdocs"
      
          location "/private/*" {
              authenticate "Restricted Area" with "/auth/mysite"
              request strip 1
          }
      }
      

      When /private/ is accessed, the server prompts for credentials and validates them against the specified htpasswd file.

      Method 2 — Inline authentication block #

      For smaller deployments or self-contained configurations, bcrypt password hashes may be embedded directly within httpd.conf.

      1. Generate bcrypt hashes

      Use smtpctl(8) to generate a bcrypt hash:

      # smtpctl encrypt 'StrongPassword'
      $2b$06$FJ5q9b1...   # example truncated
      
      1. Embed them in the config
      server "example.org" {
          listen on * tls port 443
          root "/htdocs"
      
          location "/private/*" {
              authentication "Restricted Area" {
                  user "alice" password "$2b$06$FJ5q9b1..."
                  user "bob"   password "$2b$06$X4x0e7k..."
              }
              request strip 1
          }
      }
      

      Static gzip delivery (gzip-static) #

      Enable static gzip compression to save bandwidth. When gzip encoding is accepted and a requested file also exists with a .gz suffix, httpd serves the compressed file with Content-Encoding: gzip.

      Enable at server or location scope:

      server "example.org" {
          listen on * tls port 443
          root "/htdocs/site"
          gzip-static
      
          # Optionally, restrict to an assets subtree
          location "/assets/*" {
              gzip-static
              request strip 1
              root "/htdocs/site/assets"
          }
      }
      

      Precompress assets during deployment. Example for common text types:

      # find /var/www/htdocs/site -type f \
        -name '*.css' -o -name '*.js' -o -name '*.svg' -o -name '*.html' \
        -exec gzip -k -9 {} \;
      

      Custom error responses #

      server "example.org" {
          listen on * tls port 443
          root "/htdocs/site"
      
          # Return 410 for a retired path
          location "/deprecated/*" { block return 410 }
      
          # Custom 404 page
          location * {
              block return 404 "/errors/404.html"
          }
      }
      

      FastCGI and Dynamic Content #

      Dynamic applications are served via FastCGI.

      Using slowcgi(8) (base) #

      Enable the wrapper and direct matching locations to its socket:

      # rcctl enable slowcgi
      # rcctl start slowcgi
      
      server "cgi.example.org" {
          listen on * port 80
          root "/htdocs/cgi.example"
      
          location "*.cgi" {
              fastcgi
              fastcgi socket "/run/slowcgi.sock"   # inside chroot
          }
      }
      

      Using PHP via php-fpm (packages) #

      Install and run php-fpm for the selected PHP version (service name includes the version, e.g., php83_fpm). Ensure the FastCGI socket path is visible inside /var/www.

      # pkg_add php php-fpm
      # rcctl enable php83_fpm
      # rcctl start php83_fpm
      
      server "php.example.org" {
          listen on * port 80
          root "/htdocs/php.example"
          directory index "index.php"
      
          location "*.php" {
              fastcgi
              fastcgi socket "/run/php-fpm.sock"
          }
      }
      

      Test script:

      # printf '<?php phpinfo(); ?>' > /var/www/htdocs/php.example/info.php
      

      Security Notes #

      • httpd(8) runs as _httpd and is chrooted to /var/www by default. Paths in server/location contexts are relative to the chroot. Paths in tls { certificate|key|ocsp } are absolute.

      • Avoid write access on served files for untrusted users. Use chmod(1) and chown(8) .

      • Keep an HTTP virtual server on port 80 for ACME renewal and to 301-redirect to HTTPS.

      • Open required ports in pf(4) :

        pass in on $ext_if proto tcp to (self) port { 80 443 }
        

      Service Management and Troubleshooting #

      Manage the daemon with rcctl(8) :

      # rcctl check httpd
      # rcctl restart httpd
      

      Syntax checks and logs:

      # httpd -n
      # tail -f /var/www/logs/error.log
      

      Common issues:

      • ACME challenge returns 404: verify the challenge location block and that /var/www/acme exists and is readable.
      • TLS fails to start: confirm readable certificate and key paths; see httpd.conf(5) tls block.
      • FastCGI errors: confirm backend socket path and permissions inside the chroot.
      Report a bug
      • Synopsis
        • Web Server History on OpenBSD
      • Web Server Comparison
      • Configuration
      • Logging
      • TLS with acme-client(1)
        • Prerequisites
        • Expose the ACME challenge
        • Configure acme-client(1)
        • Obtain the initial certificate
        • Enable HTTPS and redirect HTTP
        • Automated renewal
      • OCSP Stapling
      • Common Configuration Examples
        • 301 redirects (permanent)
        • URL prefix mapping and clean paths
        • HTTP Basic authentication
        • Static gzip delivery (gzip-static)
        • Custom error responses
      • FastCGI and Dynamic Content
        • Using slowcgi(8) (base)
        • Using PHP via php-fpm (packages)
      • Security Notes
      • Service Management and Troubleshooting