Integrate H2O with Let's Encrypt for automatic SSL certificates

Intermediate 30 min Apr 29, 2026 90 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up H2O HTTP/2 web server with automatic SSL certificate management using Let's Encrypt and certbot. Configure SSL termination, automatic renewal, and security hardening for production deployments.

Prerequisites

  • Root or sudo access
  • Domain name pointing to server
  • Internet connection for Let's Encrypt
  • Basic command line knowledge

What this solves

H2O is a high-performance HTTP/2 web server that needs SSL certificates for secure connections. Manual certificate management becomes tedious and error-prone, especially with Let's Encrypt's 90-day expiration cycle. This tutorial configures automatic SSL certificate provisioning and renewal using certbot, ensuring your H2O server maintains valid HTTPS certificates without manual intervention.

Step-by-step installation

Update system packages

Start by updating your package manager to ensure you get the latest security patches and software versions.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install H2O HTTP/2 server

Install H2O web server and required dependencies for SSL certificate management.

sudo apt install -y h2o curl wget openssl
sudo dnf install -y epel-release
sudo dnf install -y h2o curl wget openssl

Install certbot for Let's Encrypt

Install certbot ACME client to automate SSL certificate requests and renewals from Let's Encrypt.

sudo apt install -y certbot python3-certbot-nginx
sudo dnf install -y certbot python3-certbot-nginx

Create H2O user and directories

Create a dedicated user for H2O and set up proper directory structure with correct permissions.

sudo useradd --system --shell /bin/false --home-dir /var/lib/h2o h2o
sudo mkdir -p /etc/h2o /var/log/h2o /var/lib/h2o /var/www/html
sudo chown -R h2o:h2o /var/log/h2o /var/lib/h2o
sudo chown -R www-data:www-data /var/www/html
sudo chmod 755 /var/www/html

Configure H2O basic setup

Create the main H2O configuration file with HTTP support first. We'll add HTTPS after obtaining certificates.

user: h2o
pid-file: /var/run/h2o.pid
error-log: /var/log/h2o/error.log
access-log: /var/log/h2o/access.log

listen: 80
file.dir: /var/www/html
file.index: ["index.html", "index.htm"]

Enable gzip compression

compress: [gzip, br]

Security headers

header.add: "X-Frame-Options: DENY" header.add: "X-Content-Type-Options: nosniff" header.add: "X-XSS-Protection: 1; mode=block" header.add: "Referrer-Policy: strict-origin-when-cross-origin" hosts: "example.com": paths: "/": file.dir: /var/www/html "/.well-known": file.dir: /var/www/html/.well-known

Create test web page

Create a simple HTML page to verify H2O is working correctly.

sudo mkdir -p /var/www/html/.well-known/acme-challenge



    H2O Server


    

H2O HTTP/2 Server Running

SSL certificates will be configured with Let's Encrypt.

Create H2O systemd service

Create a systemd service file to manage H2O as a system service with proper process management.

[Unit]
Description=H2O HTTP/2 Server
After=network.target

[Service]
Type=forking
User=h2o
Group=h2o
PIDFile=/var/run/h2o.pid
ExecStart=/usr/bin/h2o -c /etc/h2o/h2o.conf -m daemon
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5

Security settings

NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ReadWritePaths=/var/log/h2o /var/run [Install] WantedBy=multi-user.target

Set proper file permissions

Configure correct ownership and permissions for H2O configuration and web files.

sudo chown -R root:h2o /etc/h2o
sudo chmod 640 /etc/h2o/h2o.conf
sudo chown -R www-data:www-data /var/www/html
sudo chmod 755 /var/www/html
sudo chmod 644 /var/www/html/index.html
sudo chmod 755 /var/www/html/.well-known
sudo chmod 755 /var/www/html/.well-known/acme-challenge

Start H2O service

Enable and start the H2O service to begin serving HTTP traffic.

sudo systemctl daemon-reload
sudo systemctl enable h2o
sudo systemctl start h2o
sudo systemctl status h2o

Configure firewall for HTTP and HTTPS

Open firewall ports for web traffic before requesting SSL certificates.

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Request Let's Encrypt certificate

Use certbot to request SSL certificates from Let's Encrypt using the webroot authentication method.

Important: Replace example.com with your actual domain name. Ensure your domain points to this server's IP address.
sudo certbot certonly --webroot \
  --webroot-path=/var/www/html \
  --email your-email@example.com \
  --agree-tos \
  --no-eff-email \
  -d example.com \
  -d www.example.com

Configure H2O with SSL certificates

Update the H2O configuration to include HTTPS support with the Let's Encrypt certificates.

user: h2o
pid-file: /var/run/h2o.pid
error-log: /var/log/h2o/error.log
access-log: /var/log/h2o/access.log

HTTP (redirect to HTTPS)

listen: 80 file.dir: /var/www/html

HTTPS with SSL certificates

listen: port: 443 ssl: certificate-file: /etc/letsencrypt/live/example.com/fullchain.pem key-file: /etc/letsencrypt/live/example.com/privkey.pem cipher-suite: ECDHE+AESCCM:ECDHE+CHACHA20:DHE+AESCCM:DHE+CHACHA20:!NULL:!aNULL:!DSS:!PSK:!SRP:!MD5:!RC4 cipher-preference: server

Global settings

compress: [gzip, br] header.add: "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload" header.add: "X-Frame-Options: DENY" header.add: "X-Content-Type-Options: nosniff" header.add: "X-XSS-Protection: 1; mode=block" header.add: "Referrer-Policy: strict-origin-when-cross-origin" hosts: "example.com": paths: "/": redirect: status: 301 url: "https://example.com/" "/.well-known": file.dir: /var/www/html/.well-known "www.example.com": paths: "/": redirect: status: 301 url: "https://www.example.com/" "/.well-known": file.dir: /var/www/html/.well-known

HTTPS virtual hosts

hosts: "example.com:443": paths: "/": file.dir: /var/www/html "/.well-known": file.dir: /var/www/html/.well-known "www.example.com:443": paths: "/": redirect: status: 301 url: "https://example.com/" "/.well-known": file.dir: /var/www/html/.well-known

Set certificate permissions for H2O

Grant H2O user access to Let's Encrypt certificates without compromising security.

sudo chgrp h2o /etc/letsencrypt/live/example.com/privkey.pem
sudo chmod 640 /etc/letsencrypt/live/example.com/privkey.pem
sudo chgrp h2o /etc/letsencrypt/live/example.com/fullchain.pem
sudo chmod 644 /etc/letsencrypt/live/example.com/fullchain.pem

Restart H2O with SSL configuration

Reload H2O to apply the new HTTPS configuration with SSL certificates.

sudo systemctl restart h2o
sudo systemctl status h2o

Configure automatic certificate renewal

Create a renewal script that reloads H2O after certificate renewal and set up automated execution.

#!/bin/bash

H2O certificate renewal hook

Set proper permissions for new certificates

chgrp h2o /etc/letsencrypt/live/*/privkey.pem chmod 640 /etc/letsencrypt/live/*/privkey.pem chgrp h2o /etc/letsencrypt/live/*/fullchain.pem chmod 644 /etc/letsencrypt/live/*/fullchain.pem

Reload H2O to use new certificates

systemctl reload h2o

Log renewal

echo "$(date): H2O certificates renewed and reloaded" >> /var/log/h2o/renewal.log
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/h2o-reload.sh
sudo touch /var/log/h2o/renewal.log
sudo chown h2o:h2o /var/log/h2o/renewal.log

Test automatic renewal

Verify that certbot renewal works correctly with the H2O reload hook.

sudo certbot renew --dry-run

Configure certificate monitoring

Create a script to monitor certificate expiration and send alerts if renewal fails.

#!/bin/bash

DOMAIN="example.com"
WARN_DAYS=30
CRIT_DAYS=7
LOGFILE="/var/log/h2o/ssl-check.log"

Get certificate expiry date

EXPIRY=$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -dates | grep notAfter | cut -d= -f2) EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s) CURRENT_EPOCH=$(date +%s) DAYS_UNTIL_EXPIRY=$(( ($EXPIRY_EPOCH - $CURRENT_EPOCH) / 86400 )) echo "$(date): Certificate expires in $DAYS_UNTIL_EXPIRY days" >> $LOGFILE if [ $DAYS_UNTIL_EXPIRY -lt $CRIT_DAYS ]; then echo "CRITICAL: SSL certificate for $DOMAIN expires in $DAYS_UNTIL_EXPIRY days!" | mail -s "SSL Certificate Critical" admin@example.com elif [ $DAYS_UNTIL_EXPIRY -lt $WARN_DAYS ]; then echo "WARNING: SSL certificate for $DOMAIN expires in $DAYS_UNTIL_EXPIRY days" | mail -s "SSL Certificate Warning" admin@example.com fi
sudo chmod +x /usr/local/bin/check-ssl-expiry.sh

Schedule SSL monitoring

Add cron job to check certificate expiry daily and ensure automatic renewal is working.

sudo crontab -e
# Check SSL certificate expiry daily at 6 AM
0 6   * /usr/local/bin/check-ssl-expiry.sh

Let's Encrypt renewal check (certbot creates this automatically)

0 0,12 * root test -x /usr/bin/certbot && perl -e 'sleep int(rand(43200))' && certbot -q renew

Verify your setup

Test that H2O is running correctly with automatic SSL certificates and proper redirects.

# Check H2O service status
sudo systemctl status h2o

Test HTTP to HTTPS redirect

curl -I http://example.com

Test HTTPS connection

curl -I https://example.com

Check SSL certificate details

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates

Verify HTTP/2 support

curl -I --http2 https://example.com

Test certificate renewal

sudo certbot certificates

Check H2O logs

sudo tail -f /var/log/h2o/access.log

Common issues

SymptomCauseFix
Certificate request failsDomain not pointing to serverUpdate DNS A record to server IP
H2O can't read certificatesWrong file permissionssudo chgrp h2o /etc/letsencrypt/live//privkey.pem && sudo chmod 640 /etc/letsencrypt/live//privkey.pem
HTTPS connection failsPort 443 blockedCheck firewall: sudo ufw status or sudo firewall-cmd --list-all
Renewal fails silentlyHook script not executablesudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/h2o-reload.sh
HTTP/2 not workingClient doesn't support HTTP/2Test with: curl --http2 -v https://example.com
Certificate not auto-renewingCron job missingReinstall certbot or check sudo crontab -l

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.