Webserver and SSL Setup - wiso-forschungslabor/hroot-documentation GitHub Wiki

Web Server & SSL/TLS Configuration (Bare-Metal)

This guide provides comprehensive instructions for deploying hroot on a Linux server without Docker, configuring a production web server (NGINX or Apache), provisioning SSL/TLS certificates (Let's Encrypt or Institutional / Custom Certificates), and setting up automated renewal.


Architecture Overview

In a bare-metal environment, Ruby on Rails runs with an application server (typically Puma or Phusion Passenger). A production-grade reverse proxy (such as NGINX or Apache) sits in front of the application server to:

  1. Terminate SSL/TLS encryption (HTTPS on port 443).
  2. Redirect unencrypted HTTP traffic (port 80) to HTTPS.
  3. Serve static assets efficiently.
  4. Manage client connections, timeouts, and file upload limits.
[ Web Browsers ] ─── HTTPS (Port 443) ───► [ NGINX / Apache ] ─── HTTP (Port 3000 / Socket) ───► [ Puma / Rails ]

Option A: NGINX Reverse Proxy + Puma (Recommended)

NGINX paired with Rails' built-in Puma application server is the modern standard for deploying Rails applications.

1. Install NGINX

On Ubuntu / Debian:

sudo apt update
sudo apt install -y nginx

2. Configure Puma as a Systemd Service

Create a systemd unit file to manage the Puma background process automatically:

sudo nano /etc/systemd/system/hroot.service

Paste the following service definition (adjust /home/hroot/hroot and username hroot to match your installation path):

[Unit]
Description=HROOT Rails Application Server (Puma)
After=network.target mysql.service

[Service]
Type=simple
User=hroot
WorkingDirectory=/home/hroot/hroot
Environment=RAILS_ENV=production
# If using rbenv, ensure the PATH points to the rbenv shims:
Environment="PATH=/home/hroot/.rbenv/shims:/home/hroot/.rbenv/bin:/usr/local/bin:/usr/bin:/bin"

ExecStart=/home/hroot/.rbenv/shims/bundle exec puma -C config/puma.rb -b tcp://127.0.0.1:3000
Restart=always
RestartSec=5

# Resource limits
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now hroot
sudo systemctl status hroot

3. Configure NGINX Site

Create a dedicated NGINX configuration file for your hroot domain:

sudo nano /etc/nginx/sites-available/hroot.conf

Insert the following configuration:

# Upstream Puma application server
upstream hroot_puma {
    server 127.0.0.1:3000 fail_timeout=0;
}

# HTTP Server: Redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name hroot.example.org;

    # Let's Encrypt ACME challenge location
    location /.well-known/acme-challenge/ {
        root /var/www/html;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS Server: SSL Termination and Reverse Proxy
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name hroot.example.org;

    # Path to static public files
    root /home/hroot/hroot/public;

    # SSL Certificate files (configured manually or via Certbot)
    ssl_certificate /etc/ssl/certs/hroot_fullchain.pem;
    ssl_certificate_key /etc/ssl/private/hroot_privkey.pem;

    # Modern TLS Security Parameters
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # Security Headers
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Max upload body size for participant attachments & imports
    client_max_body_size 50M;

    # Serve precompiled static assets directly with long cache headers
    location ~ ^/(assets|packs)/ {
        gzip_static on;
        expires max;
        add_header Cache-Control public;
    }

    # ActionCable / WebSocket support (for live updates & waiting rooms)
    location /cable {
        proxy_pass http://hroot_puma;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    # Proxy all dynamic requests to Puma
    location / {
        proxy_pass http://hroot_puma;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port 443;
        proxy_redirect off;
    }

    error_page 500 502 503 504 /500.html;
}

Enable the configuration and reload NGINX:

sudo ln -sf /etc/nginx/sites-available/hroot.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Option B: Apache 2.4 + Phusion Passenger

If your university or organization requires Apache, use Phusion Passenger with mod_ssl.

1. Install Apache & Passenger

Follow the official Passenger APT repository instructions:

sudo apt install -y apache2 libapache2-mod-passenger
sudo a2enmod ssl headers rewrite

2. Configure Apache VirtualHosts

Create /etc/apache2/sites-available/hroot.conf:

# Port 80: Redirect to HTTPS
<VirtualHost *:80>
    ServerName hroot.example.org
    ServerAdmin [email protected]

    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

# Port 443: SSL and Passenger Execution
<VirtualHost *:443>
    ServerName hroot.example.org
    ServerAdmin [email protected]

    DocumentRoot /home/hroot/hroot/public

    <Directory /home/hroot/hroot/public>
        AllowOverride all
        Options -MultiViews
        Require all granted
    </Directory>

    # Passenger Rails Settings
    PassengerEnabled on
    PassengerAppEnv production
    PassengerRuby /home/hroot/.rbenv/shims/ruby

    # SSL / TLS Settings
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/hroot_fullchain.pem
    SSLCertificateKeyFile /etc/ssl/private/hroot_privkey.pem

    # Security Headers
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Header always set X-Content-Type-Options nosniff
    Header always set X-Frame-Options SAMEORIGIN
</VirtualHost>

Enable the site and reload Apache:

sudo a2ensite hroot.conf
sudo apache2ctl configtest
sudo systemctl restart apache2

SSL Certificate Provisioning

Method 1: Automated Free Certificates via Let's Encrypt (Certbot)

Let's Encrypt provides free, trusted, automatically renewed SSL certificates.

1. Install Certbot

On Ubuntu / Debian:

sudo apt update
# For NGINX:
sudo apt install -y certbot python3-certbot-nginx

# For Apache:
sudo apt install -y certbot python3-certbot-apache

2. Obtain and Install Certificate

Run Certbot interactively:

  • For NGINX:
    sudo certbot --nginx -d hroot.example.org
  • For Apache:
    sudo certbot --apache -d hroot.example.org

Certbot will automatically verify your domain via HTTP-01 challenge, retrieve the certificate, update your webserver configuration, and test syntax.

3. Automated Renewal Verification

Certbot automatically installs a systemd timer that checks certificate validity twice daily.

To verify the timer is active:

systemctl status certbot.timer

To test that renewal works seamlessly without waiting for expiration:

sudo certbot renew --dry-run

Tip

If you need to reload your web server automatically when a certificate is renewed, Certbot supports renewal hooks:

# Place a post-renewal hook script:
echo '#!/bin/sh' | sudo tee /etc/letsencrypt/renewal-hooks/post/reload-webserver.sh
echo 'systemctl reload nginx || systemctl reload apache2' | sudo tee -a /etc/letsencrypt/renewal-hooks/post/reload-webserver.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-webserver.sh

Method 2: Institutional / Custom Certificates (e.g. DFN-PKI, GÉANT, Sectigo)

Many universities and research institutes issue official organization-validated certificates through institutional PKIs.

1. Prepare Certificate Files

You will typically receive:

  • Server Certificate: certificate.crt
  • Intermediate CA Bundle: chain.crt
  • Private Key: Generated when creating the CSR (e.g., server.key)

Combine the server certificate and intermediate bundle into a single fullchain.pem:

cat certificate.crt chain.crt > /etc/ssl/certs/hroot_fullchain.pem

Move the private key to the secured private directory:

sudo cp server.key /etc/ssl/private/hroot_privkey.pem
sudo chmod 600 /etc/ssl/private/hroot_privkey.pem
sudo chown root:root /etc/ssl/private/hroot_privkey.pem

2. Configure Web Server

  • In NGINX:
    ssl_certificate /etc/ssl/certs/hroot_fullchain.pem;
    ssl_certificate_key /etc/ssl/private/hroot_privkey.pem;
  • In Apache:
    SSLCertificateFile /etc/ssl/certs/hroot_fullchain.pem
    SSLCertificateKeyFile /etc/ssl/private/hroot_privkey.pem

Rails Application Configuration (FORCE_SSL)

Once SSL is configured on your reverse proxy, inform the Rails application to enforce HTTPS cookies and HTTP Strict Transport Security (HSTS).

In your .env file (/home/hroot/hroot/.env):

FORCE_SSL=true

When FORCE_SSL=true:

  • Cookies are sent exclusively over encrypted connections (Secure flag).
  • HSTS (Strict-Transport-Security) headers prevent downgrade attacks.
  • Internal redirects ensure all URLs generated by the app use https://.

After modifying .env, restart Puma or Passenger:

# Puma:
sudo systemctl restart hroot

# Passenger:
sudo passenger-config restart-app /home/hroot/hroot

Verification & Troubleshooting

1. Test HTTPS Headers from Terminal

curl -I https://hroot.example.org

Expected response headers:

HTTP/2 200
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff

2. Verify Port Forwarding

Verify that Puma is listening only on 127.0.0.1:3000 (not accessible directly from the public internet):

ss -tuln | grep 3000

Output should be: 127.0.0.1:3000.

3. SSL Expiration Check

Check certificate validity date from the command line:

openssl s_client -connect hroot.example.org:443 -servername hroot.example.org 2>/dev/null | openssl x509 -noout -dates
⚠️ **GitHub.com Fallback** ⚠️