Skip to content

Instantly share code, notes, and snippets.

@anon987654321
Last active November 11, 2024 13:35
Show Gist options
  • Select an option

  • Save anon987654321/948d5957faa1bdfd75737b0af3da6ab6 to your computer and use it in GitHub Desktop.

Select an option

Save anon987654321/948d5957faa1bdfd75737b0af3da6ab6 to your computer and use it in GitHub Desktop.

OpenBSD Rails Server Setup

This setup script automates the deployment of an OpenBSD VPS as a secure, optimized hosting environment for Ruby on Rails applications. It handles essential installations, security configurations, domain management, and SSL certification, creating a production-ready server setup.

Choose OpenBSD for your Unix needs. OpenBSD -- the world's simplest and most secure Unix-like OS. A safe alternatve to the frequent vulnerabilities and overengineering of Linux and related software like NGiNX & Apache (https://openbsd.org/papers/httpd-asiabsdcon2015.pdf), OpenSSL, iptables/nftables, systemd, BIND, Postfix, Docker and so on.

OpenBSD -- the cleanest kernel, the cleanest userland and the cleanest configuration syntax.

Features

  • Automated Software Installation: Installs necessary components, including ruby, postgresql-server, redis, and varnish for web acceleration.
  • Dynamic Port Management: Prevents port conflicts through automated port assignment.
  • Firewall Configuration (pf.conf(5) and pfctl(8)): Configures OpenBSD’s Packet Filter to secure the server by controlling access and limiting vulnerabilities.
  • Traffic Routing with relayd(8): Configures relayd as a reverse proxy to manage HTTP/HTTPS traffic, directing it securely to Rails applications.
  • DNS Management with nsd(8): Uses nsd to configure DNS zones for each domain and subdomain, with DNSSEC enabled for added security.
  • SSL Automation with acme-client(8): Uses acme-client with Let’s Encrypt for automated SSL certificate issuance and renewal.
  • Rails Application Management (rc.d(8) scripts): Generates startup scripts for each Rails application, enabling seamless control through rcctl(8).

Configuration Details

Domains and Subdomains

The script supports multiple domains and subdomains, specified in the ALL_DOMAINS list. Each domain configuration includes DNS records, SSL certificates, and relayd routing rules.

Port Management

The generate_random_port() function assigns available ports dynamically to avoid conflicts across services such as relayd and Rails applications.

SSL Certificates and Secure Connections

Using acme-client(8) with Let’s Encrypt, the script automatically handles SSL certificate issuance and renewal for all domains, ensuring secure HTTPS connections. OpenBSD’s httpd(8) is configured to respond to ACME challenges, automating the SSL setup.

Firewall Configuration with pf.conf(5) and pfctl(8)

The firewall (pf) is configured to control inbound and outbound traffic, enhancing server security. It includes brute-force protection for SSH, rate-limiting, and access controls for DNS, HTTP, and HTTPS, ensuring only authorized access.

Traffic Management with relayd(8) and relayd.conf(5)

relayd directs HTTP and HTTPS traffic with two specific protocols:

  • ACME Challenge Routing: Routes SSL certificate validation requests to acme-client.
  • Application Request Routing: Forwards user traffic to the Rails applications, enhancing scalability and security.

DNS Management with nsd(8) and nsd.conf(5)

The configure_nsd function automates DNS zone configuration for each domain, enabling DNSSEC to ensure integrity and authenticity of DNS records.

Rails Application Startup and Management with rc.d(8) and rcctl(8)

Each Rails application is configured with a startup script in /etc/rc.d/. These scripts allow rcctl to manage application start and stop processes, using Falcon as the application server.

#!/usr/bin/env zsh
set -e
OPENBSD_AMSTERDAM_IP="X.X.X.X"
ALL_DOMAINS=(
"mydomain.com:subdomain1, subdomain2"
)
RAILS_APPS=("myapp1" "myapp2")
install_packages() {
packages=("ruby-3.3.5" "postgresql-server" "dnscrypt-proxy" "sshguard" "monit" "redis" "varnish" "ldns-utils")
for package in "${packages[@]}"; do
doas pkg_add -UI "$package"
done
}
generate_random_port() {
while true; do
port=$((2000 + RANDOM % 63000))
if ! netstat -an | grep -q "\.$port "; then
echo "$port"
return
fi
done
}
configure_pf() {
cat <<EOF | doas tee /etc/pf.conf > /dev/null
set skip on lo
block all
# Brute-force protection
table <bruteforce> persist
block in quick from <bruteforce>
# Allow SSH with rate limiting and overload protection
pass in on vio0 proto tcp to port 22 keep state (max-src-conn 50, max-src-conn-rate 20/60, overload <bruteforce> flush global, no-sync)
# Allow DNS
pass in on vio0 proto { tcp, udp } from any to port 53 keep state
# Allow HTTP/HTTPS with logging
pass in log on vio0 proto tcp to port { 80, 443 } keep state
# Allow all outgoing traffic
pass out on vio0 keep state
# Relay rules
anchor "relayd/*"
# Log blocked connections for troubleshooting
block log all
EOF
doas pfctl -nf /etc/pf.conf
doas pfctl -f /etc/pf.conf
}
configure_nsd() {
cat <<EOF | doas tee /var/nsd/etc/nsd.conf > /dev/null
server:
ip-address: $OPENBSD_AMSTERDAM_IP
hide-version: yes
ip4-only: yes
verbosity: 2
zonesdir: "/zones"
EOF
for domain_info in "${ALL_DOMAINS[@]}"; do
domain="${domain_info%%:*}"
# Cleanup old keys
doas rm -f /var/nsd/zones/K${domain}.*
# Set serial number based on the current date/time
serial=$(date +"%Y%m%d%H")
# Generate DNSSEC keys for the domain and set permissions
doas ldns-keygen -a ECDSAP256SHA256 -b 256 -r /dev/urandom "$domain"
key_files=(/var/nsd/zones/K${domain}.*)
doas chown root:_nsd "${key_files[@]}"
doas chmod 640 "${key_files[@]}"
# Extract DNSKEY using zsh
dnskey_line=$(doas grep -h "DNSKEY" "${key_files[@]}")
read -r _ _ _ dnskey _ <<< "$dnskey_line"
# Create or update the zone file with the new serial
cat <<ZONE | doas tee "/var/nsd/zones/$domain.zone" > /dev/null
\$ORIGIN $domain.
\$TTL 3600
@ IN SOA ns.brgen.no. hostmaster.$domain. (
$serial ; Serial
3600 ; Refresh
900 ; Retry
1209600 ; Expire
3600 ; Minimum TTL
)
@ IN NS ns.brgen.no.
@ IN NS ns.hyp.net.
@ IN A $OPENBSD_AMSTERDAM_IP
@ IN CAA 0 issue "letsencrypt.org"
@ IN DNSKEY 257 3 13 "$dnskey"
ZONE
# Generate DS records for each DNSSEC key
for key_file in /var/nsd/zones/K${domain}+*.key; do
ds_record=$(doas ldns-key2ds "$key_file")
echo "Register the following DS record with your registrar for domain $domain:"
echo "$ds_record"
done
# Append the zone configuration for nsd
cat <<EOF >> /var/nsd/etc/nsd.conf
zone:
name: "$domain"
zonefile: "/zones/$domain.zone.signed"
allow-notify: ns.hyp.net NOKEY
provide-xfr: ns.hyp.net NOKEY
EOF
# Sign and verify the zone
doas ldns-signzone -n -p -o "$domain" "/var/nsd/zones/$domain.zone"
doas ldns-verify-zone "/var/nsd/zones/$domain.zone.signed"
done
doas nsd-checkconf /var/nsd/etc/nsd.conf
# Check each zone file
for domain_info in "${ALL_DOMAINS[@]}"; do
domain="${domain_info%%:*}"
doas nsd-checkzone "$domain" "/zones/$domain.zone.signed"
done
doas rcctl enable nsd
doas rcctl restart nsd
}
configure_httpd_and_acme_client() {
cat <<EOF | doas tee /etc/httpd.conf > /dev/null
server "acme" {
listen on $OPENBSD_AMSTERDAM_IP port 80
location "/.well-known/acme-challenge/*" {
root "/var/www/acme"
request strip 2
}
}
EOF
doas rcctl enable httpd
doas rcctl restart httpd
for domain_info in "${ALL_DOMAINS[@]}"; do
domain="${domain_info%%:*}"
[[ -z "$domain" ]] && continue
cat <<EOF | doas tee -a /etc/acme-client.conf > /dev/null
domain $domain {
domain key "/etc/ssl/private/$domain.key"
domain fullchain "/etc/ssl/$domain.fullchain.pem"
sign with default
}
EOF
done
doas acme-client -n
for domain_info in "${ALL_DOMAINS[@]}"; do
domain="${domain_info%%:*}"
[[ -z "$domain" ]] && continue
if ! openssl x509 -checkend 2592000 -noout -in "/etc/ssl/$domain.fullchain.pem"; then
doas acme-client -v "$domain"
else
echo "Certificate for $domain is still valid. Skipping renewal."
fi
done
}
configure_relayd() {
acme_client_port=$(generate_random_port)
varnish_port=$(generate_random_port)
cat <<EOF | doas tee /etc/relayd.conf > /dev/null
log connection
# Define a table for routing ACME client challenges to localhost
table <acme_client> { 127.0.0.1:$acme_client_port }
# HTTP protocol for ACME challenges
http protocol "filter_challenge" {
pass request path "/.well-known/acme-challenge/*" forward to <acme_client>
}
# HTTP protocol for backend Varnish server
http protocol "varnish_backend" {
# Add X-Forwarded-By header to track the original server address
match request header set "X-Forwarded-By" value "\$SERVER_ADDR:\$SERVER_PORT"
# Add X-Forwarded-For header to record the client IP address
match request header set "X-Forwarded-For" value "\$REMOTE_ADDR"
# Add HSTS header to ensure secure connections
match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
# Prevent clickjacking by restricting framing to the same origin
match response header set "X-Frame-Options" value "SAMEORIGIN"
# Upgrade all requests to HTTPS
match response header set "Content-Security-Policy" value "upgrade-insecure-requests"
# Prevent MIME-type sniffing for enhanced security
match response header set "X-Content-Type-Options" value "nosniff"
# Ensure no referrer information is sent to external domains
match response header set "Referrer-Policy" value "no-referrer"
# Limit geolocation permissions to approved origins only
match response header set "Permissions-Policy" value "geolocation=*"
}
relay "acme_relay" {
listen on $OPENBSD_AMSTERDAM_IP port 80
forward to 127.0.0.1 port $acme_client_port protocol "filter_challenge"
}
relay "https_relay" {
listen on $OPENBSD_AMSTERDAM_IP port 443 tls
protocol "varnish_backend"
forward to 127.0.0.1 port $varnish_port
}
EOF
doas relayd -n -f /etc/relayd.conf
doas rcctl enable relayd
doas rcctl restart relayd
}
configure_varnish() {
varnish_port=$(generate_random_port)
falcon_backend_port=$(generate_random_port)
# Varnish configuration to use Falcon as backend on port $falcon_backend_port
cat <<EOF | doas tee /etc/varnish/default.vcl > /dev/null
vcl 4.0;
backend default {
.host = "127.0.0.1";
.port = "$falcon_backend_port"; # Falcon server port for Rails apps
}
# Cache assets and strip cookies for improved caching
sub vcl_recv {
if (req.url ~ "^/assets/") {
unset req.http.cookie;
}
}
# Set TTL for cached responses
sub vcl_backend_response {
if (beresp.status == 200) {
set beresp.ttl = 1h;
}
}
EOF
# Start Varnish on dynamically assigned port $varnish_port
doas rcctl enable varnishd
doas rcctl set varnishd flags "-a :$varnish_port -b localhost:$falcon_backend_port"
doas rcctl start varnishd
}
configure_startup_scripts() {
for app in "${RAILS_APPS[@]}"; do
if ! grep -q "^$app:" /etc/master.passwd; then
doas useradd -m -s /bin/ksh "$app"
fi
# Assign a unique random port for each Rails app instance
backend_port=$(generate_random_port)
# Falcon startup script
cat <<EOF | doas tee "/etc/rc.d/$app" > /dev/null
#!/bin/ksh
# Startup script for the $app application on port $backend_port
daemon="/bin/ksh -c 'cd /home/$app/$app && export RAILS_ENV=production && /usr/local/bin/bundle exec falcon serve -b tcp://127.0.0.1:$backend_port'"
daemon_user="$app"
# Unveil limits file access to only essential directories
unveil -r /home/$app/$app
unveil /var/www/log
unveil /etc
unveil
# Pledge restricts system calls for enhanced security
pledge stdio rpath wpath cpath inet
. /etc/rc.d/rc.subr
rc_cmd \$1
EOF
doas chmod +x "/etc/rc.d/$app"
doas rcctl enable "$app"
doas rcctl start "$app"
done
}
main() {
install_packages
configure_pf
configure_nsd
configure_httpd_and_acme_client
configure_relayd
configure_varnish
configure_startup_scripts
echo "OpenBSD VPS setup for Ruby on Rails applications completed successfully."
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment