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
Install H2O HTTP/2 server
Install H2O web server and required dependencies for SSL certificate management.
sudo apt 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
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
Request Let's Encrypt certificate
Use certbot to request SSL certificates from Let's Encrypt using the webroot authentication method.
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
| Symptom | Cause | Fix |
|---|---|---|
| Certificate request fails | Domain not pointing to server | Update DNS A record to server IP |
| H2O can't read certificates | Wrong file permissions | sudo chgrp h2o /etc/letsencrypt/live//privkey.pem && sudo chmod 640 /etc/letsencrypt/live//privkey.pem |
| HTTPS connection fails | Port 443 blocked | Check firewall: sudo ufw status or sudo firewall-cmd --list-all |
| Renewal fails silently | Hook script not executable | sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/h2o-reload.sh |
| HTTP/2 not working | Client doesn't support HTTP/2 | Test with: curl --http2 -v https://example.com |
| Certificate not auto-renewing | Cron job missing | Reinstall certbot or check sudo crontab -l |
Next steps
- Configure H2O HTTP/2 server load balancing with health checks and SSL termination
- Configure NGINX reverse proxy with SSL termination and load balancing for high availability
- Set up H2O with Docker containers and automatic SSL certificates
- Implement H2O rate limiting and DDoS protection with advanced security rules
- Monitor H2O performance with Prometheus and Grafana dashboards
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
DOMAIN=""
EMAIL=""
TOTAL_STEPS=12
# Usage function
usage() {
echo "Usage: $0 -d DOMAIN -e EMAIL"
echo " -d DOMAIN Domain name for SSL certificate"
echo " -e EMAIL Email address for Let's Encrypt registration"
echo "Example: $0 -d example.com -e admin@example.com"
exit 1
}
# Error handling and cleanup
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop h2o 2>/dev/null || true
systemctl disable h2o 2>/dev/null || true
userdel h2o 2>/dev/null || true
rm -rf /etc/h2o /var/lib/h2o /var/log/h2o 2>/dev/null || true
echo -e "${YELLOW}Cleanup completed${NC}"
}
trap cleanup ERR
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
}
# Parse command line arguments
parse_args() {
while getopts "d:e:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then
usage
fi
}
# Detect distribution and set package manager
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
WEB_USER="www-data"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
WEB_USER="apache"
FIREWALL_CMD="firewall-cmd"
# Enable EPEL for RHEL-based systems
dnf install -y epel-release 2>/dev/null || true
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
WEB_USER="apache"
FIREWALL_CMD="firewall-cmd"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
}
# Update system packages
update_system() {
echo -e "${GREEN}[1/$TOTAL_STEPS] Updating system packages...${NC}"
eval $PKG_UPDATE
}
# Install H2O and dependencies
install_h2o() {
echo -e "${GREEN}[2/$TOTAL_STEPS] Installing H2O HTTP/2 server...${NC}"
$PKG_INSTALL h2o curl wget openssl
}
# Install certbot
install_certbot() {
echo -e "${GREEN}[3/$TOTAL_STEPS] Installing certbot for Let's Encrypt...${NC}"
$PKG_INSTALL certbot python3-certbot-nginx
}
# Create H2O user and directories
setup_h2o_user() {
echo -e "${GREEN}[4/$TOTAL_STEPS] Creating H2O user and directories...${NC}"
useradd --system --shell /bin/false --home-dir /var/lib/h2o h2o 2>/dev/null || true
mkdir -p /etc/h2o /var/log/h2o /var/lib/h2o /var/www/html/.well-known/acme-challenge
chown -R h2o:h2o /var/log/h2o /var/lib/h2o
chown -R $WEB_USER:$WEB_USER /var/www/html
chmod 755 /var/www/html
chmod 755 /var/www/html/.well-known
chmod 755 /var/www/html/.well-known/acme-challenge
}
# Configure H2O
configure_h2o() {
echo -e "${GREEN}[5/$TOTAL_STEPS] Configuring H2O...${NC}"
cat > /etc/h2o/h2o.conf << EOF
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"]
compress: [gzip, br]
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:
"$DOMAIN":
paths:
"/":
file.dir: /var/www/html
"/.well-known":
file.dir: /var/www/html/.well-known
EOF
}
# Create test web page
create_test_page() {
echo -e "${GREEN}[6/$TOTAL_STEPS] Creating test web page...${NC}"
cat > /var/www/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>H2O Server</title>
</head>
<body>
<h1>H2O HTTP/2 Server Running</h1>
<p>SSL certificates will be configured with Let's Encrypt.</p>
</body>
</html>
EOF
chmod 644 /var/www/html/index.html
}
# Create systemd service
create_systemd_service() {
echo -e "${GREEN}[7/$TOTAL_STEPS] Creating H2O systemd service...${NC}"
cat > /etc/systemd/system/h2o.service << 'EOF'
[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
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/log/h2o /var/run
[Install]
WantedBy=multi-user.target
EOF
}
# Set proper permissions
set_permissions() {
echo -e "${GREEN}[8/$TOTAL_STEPS] Setting file permissions...${NC}"
chown -R root:h2o /etc/h2o
chmod 640 /etc/h2o/h2o.conf
chown -R $WEB_USER:$WEB_USER /var/www/html
chmod 755 /var/www/html
chmod 644 /var/www/html/index.html
}
# Start H2O service
start_h2o() {
echo -e "${GREEN}[9/$TOTAL_STEPS] Starting H2O service...${NC}"
systemctl daemon-reload
systemctl enable h2o
systemctl start h2o
sleep 2
systemctl status h2o --no-pager
}
# Configure firewall
configure_firewall() {
echo -e "${GREEN}[10/$TOTAL_STEPS] Configuring firewall...${NC}"
case "$FIREWALL_CMD" in
"ufw")
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
;;
"firewall-cmd")
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
;;
esac
}
# Obtain SSL certificate
obtain_ssl_cert() {
echo -e "${GREEN}[11/$TOTAL_STEPS] Obtaining SSL certificate from Let's Encrypt...${NC}"
certbot certonly --webroot -w /var/www/html -d $DOMAIN --email $EMAIL --agree-tos --non-interactive
# Update H2O config with SSL
cat > /etc/h2o/h2o.conf << EOF
user: h2o
pid-file: /var/run/h2o.pid
error-log: /var/log/h2o/error.log
access-log: /var/log/h2o/access.log
listen: 80
listen:
port: 443
ssl:
certificate-file: /etc/letsencrypt/live/$DOMAIN/fullchain.pem
key-file: /etc/letsencrypt/live/$DOMAIN/privkey.pem
file.dir: /var/www/html
file.index: ["index.html", "index.htm"]
compress: [gzip, br]
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"
header.add: "Strict-Transport-Security: max-age=31536000; includeSubDomains"
hosts:
"$DOMAIN":
paths:
"/":
redirect:
status: 301
url: "https://$DOMAIN/"
"https://$DOMAIN":
paths:
"/":
file.dir: /var/www/html
"/.well-known":
file.dir: /var/www/html/.well-known
EOF
# Setup auto-renewal
(crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet --post-hook 'systemctl reload h2o'") | crontab -
systemctl reload h2o
}
# Verify installation
verify_installation() {
echo -e "${GREEN}[12/$TOTAL_STEPS] Verifying installation...${NC}"
if systemctl is-active --quiet h2o; then
echo -e "${GREEN}✓ H2O service is running${NC}"
else
echo -e "${RED}✗ H2O service failed to start${NC}"
exit 1
fi
if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then
echo -e "${GREEN}✓ SSL certificate obtained successfully${NC}"
else
echo -e "${RED}✗ SSL certificate not found${NC}"
exit 1
fi
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}Your H2O server with Let's Encrypt SSL is now running at:${NC}"
echo -e " HTTP: http://$DOMAIN (redirects to HTTPS)"
echo -e " HTTPS: https://$DOMAIN"
echo -e "${YELLOW}SSL certificate will auto-renew via cron job${NC}"
}
# Main execution
main() {
check_root
parse_args "$@"
detect_distro
update_system
install_h2o
install_certbot
setup_h2o_user
configure_h2o
create_test_page
create_systemd_service
set_permissions
start_h2o
configure_firewall
obtain_ssl_cert
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh