Skip to content

Instantly share code, notes, and snippets.

@j-manu
Forked from basicfeatures/rails_falcon_openbsd.md
Created March 3, 2023 04:35
Show Gist options
  • Save j-manu/59e6c7cd2021a437813b4478f380d2d9 to your computer and use it in GitHub Desktop.
Save j-manu/59e6c7cd2021a437813b4478f380d2d9 to your computer and use it in GitHub Desktop.

OpenBSD logo     Rails logo     Falcon logo


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 (NGiNX & Apache (httpd-asiabsdcon2015.pdf), OpenSSL, iptables/nftables, systemd, BIND, Postfix, Docker etc.)

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


Ruby On Rails and Falcon run as an unprivileged user, so incase the app gets hacked, the root system will remain unaffected. This user also only has ownership of tmp/ and log/ making it unable to modify any of its runtime files.

  • relayd(8) does reverse proxying and TLS termination for Falcon on port HTTPS/443
  • httpd(8) listens for ACME challenges from Let's Encrypt on port HTTP/80 and passes them on to acme-client(1)
  • pf(4) firewall locks down the system, and uses pf-badhost to block out roughly 600.000.000 spam IPs
  • Thanks to ruby-pledge Rails now uses pledge(2) to kill processes that violate its promises, while unveil(2) makes parts of the filesystem that are closed off to the public seem non-existant.

Create unprivileged user and group for the app:

root# adduser -group USER -batch myappy

Create privileged/wheel user with doas(1) root access:

root# adduser -group WHEEL -batch dev
root# echo "permit nopass :wheel" >> /etc/doas.conf

Ruby On Rails

root# pkg_add ruby

Set gem path in OpenBSD's default KornShell:

myappy% echo "PATH=$PATH:$HOME/.local/share/gem/ruby/3.1/bin; export PATH" >> ~/.kshrc
myappy% . ~/.kshrc

Nokogiri:

root# pkg_add libxslt

myappy% gem install --user-install nokogiri -- --use-system-libraries
myappy% bundle config build.nokogiri --use-system-libraries

Rails/Falcon:

myappy% gem install --user-install rails
myappy% gem install --user-install falcon
myappy% gem install --user-install foreman

7.1-alpha:

myappy% gem install --user-install specific_install

myappy% gem git_install --user-install https://github.com/rails/rails.git -d activesupport
myappy% gem git_install --user-install https://github.com/rails/rails.git -d activemodel
myappy% gem git_install --user-install https://github.com/rails/rails.git -d activerecord
myappy% gem git_install --user-install https://github.com/rails/rails.git -d activejob
myappy% gem git_install --user-install https://github.com/rails/rails.git -d actionview
myappy% gem git_install --user-install https://github.com/rails/rails.git -d actionpack
myappy% gem git_install --user-install https://github.com/rails/rails.git -d activestorage
myappy% gem git_install --user-install https://github.com/rails/rails.git -d actiontext
myappy% gem git_install --user-install https://github.com/rails/rails.git -d actioncable
myappy% gem git_install --user-install https://github.com/rails/rails.git -d actionmailbox
myappy% gem git_install --user-install https://github.com/rails/rails.git -d actionmailer
myappy% gem git_install --user-install https://github.com/rails/rails.git -d railties
myappy% gem git_install --user-install https://github.com/rails/rails.git

PostgreSQL

root# pkg_add postgresql-server

root# rcctl enable postgresql
root# doas -u _postgresql initdb -D /var/postgresql/data/ -U postgres
root# rcctl start postgresql
root# doas -u _postgresql psql -U postgres

CREATE ROLE <user> LOGIN SUPERUSER PASSWORD '<password>';

Redis

root# pkg_add redis

root# rcctl enable redis
root# rcctl start redis

JavaScript

root# pkg_add node
root# npm install --global yarn

CSS

root# pkg_add sass

Images

ruby-vips for ultra-fast image processing:

root# pkg_add libvips glib2 gobject-introspection

root# ln -sf /usr/local/lib/libvips.so.0.0 /usr/local/lib/libvips.so.42
root# ln -sf /usr/local/lib/libglib-2.0.so.4201.8 /usr/local/lib/glib-2.0.so.0
root# ln -sf /usr/local/lib/libgobject-2.0.so.4200.15 /usr/local/lib/libgobject-2.0.so.0

Falcon

falcon.rb (production)

#!/usr/bin/env falcon-host31

load :rack

hostname = File.basename(__dir__)
port = 12345

rack hostname do
  append preload "preload.rb"
  cache false
  count ENV.fetch("FALCON_COUNT", 1).to_i
  endpoint Async::HTTP::Endpoint
    .parse("http://0.0.0.0:#{ port }")
    .with(protocol: Async::HTTP::Protocol::HTTP11)
  # .with(protocol: Async::HTTP::Protocol::HTTP2)
end

preload.rb

require_relative "config/environment"

Procfile.dev (development)

web: bundle exec falcon31 serve --threaded --bind http://0.0.0.0:6969
js: yarn build --watch
css: yarn build:css --watch

Startup-script

/etc/rc.d/myappy

#!/bin/ksh

# Rails/Falcon startup script
#   https://man.openbsd.org/rc.d
#   https://github.com/openbsd/ports/blob/master/infrastructure/templates/rc.template

app_name="myappy"
daemon_user="myappy"
daemon="/home/myappy/.local/share/gem/ruby/3.1/bin/falcon-host31"
daemon_flags="/home/myappy/myappy/falcon.rb"
daemon_execdir="/home/myappy/myappy/"
# daemon_logger="daemon.info"
daemon_rtable=0

. /etc/rc.d/rc.subr

pexp="$(eval echo "ruby31: "${daemon}${daemon_flags:+ ${daemon_flags}})"

rc_bg=YES

rc_reload=YES
rc_reload_signal=HUP

rc_stop=YES
rc_stop_signal=TERM

rc_start() {
  rc_exec "RAILS_ENV=production bundle exec $daemon $daemon_flags 2>&1 | logger -t $app_name &"
}

rc_check() {
  pgrep -T "${daemon_rtable}" -q -xf "${pexp}"
}

rc_reload() {
  pkill -${rc_reload_signal} -T "${daemon_rtable}" -xf "${pexp}"
}

rc_stop() {
  pkill -${rc_stop_signal} -T "${daemon_rtable}" -xf "${pexp}"
}

rc_cmd "$1"

Reverse proxy

/etc/relayd.conf

egress="<IP>"

table <acme_client> { 127.0.0.1 }
acme_client_port="23456"

table <myappy> { 127.0.0.1 }
myappy_port="12345"

http protocol "filter_challenge" {
  pass request path "/.well-known/acme-challenge/*" forward to <acme_client>
}

relay "http_relay" {
  listen on $egress port http
  protocol "filter_challenge"
  forward to <acme_client> port $acme_client_port
}

http protocol "falcon" {

  # Preserve IPs for Falcon
  match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"    
  match request header set "X-Forwarded-For" value "$REMOTE_ADDR"

  # Best practice security headers
  # https://securityheaders.com/
  match response header set "Cache-Control" value "max-age=1814400"
  match response header set "Content-Security-Policy" value "upgrade-insecure-requests; default-src https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
  match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
  # match response header set "Frame-Options" value "SAMEORIGIN"
  match response header set "Referrer-Policy" value "strict-origin"
  match response header set "Feature-Policy" value "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'"
  # match response header set "X-Content-Type-Options" value "nosniff"
  # match response header set "X-Download-Options" value "noopen"
  # match response header set "X-Frame-Options" value "SAMEORIGIN"
  match response header set "X-Robots-Tag" value "index, nofollow"
  match response header set "X-XSS-Protection" value "1; mode=block"

  # --
  
  pass request header "Host" value "myappy.com" forward to <myappy>
  pass request header "Host" value "www.myappy.com" forward to <myappy>
  tls keypair "myappy.com"

  # --

  # Redis/Action Cable/StimulusReflex
  http websockets
}

relay "https_relay" {
  listen on $egress port https tls
  protocol "falcon"
  forward to <myappy> port $myappy_port
}

Let's Encrypt HTTPS

/etc/httpd.conf

types {
  include "/usr/share/misc/mime.types"
}

server "myappy.com" {
  alias "www.myappy.com"
  listen on localhost port 12345
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location "*" {
    block return 301 "https://myappy.com$REQUEST_URI"
  }
}

/etc/acme-client.conf

authority letsencrypt {
  api url "https://acme-v02.api.letsencrypt.org/directory"
  account key "/etc/ssl/private/letsencrypt.key"
}

domain myappy.com {
  alternative names { www.myappy.com }
  domain key "/etc/ssl/private/myappy.com.key"
  domain full chain certificate "/etc/ssl/myappy.com.crt"
  sign with letsencrypt
}

mkcert.sh

#!/usr/bin/env ksh
# GENERATES TLS-CERTIFICATES AND CRONTABS

list=(
  "domain1.com"
  "domain2.com"
  "domain3.com"
  "domain4.com"
)

for domain in $list; do
  acme-client -v $domain

  # Check for cert once a week
  # Format: minute hour day-of-month month day-of-week
  (crontab -l; echo "~ ~ * * ~ acme-client $domain && rcctl reload relayd") | crontab -

  sleep 12
done

PF firewall

Install pf-badhost. Optionally add _TOR_BLOCK_ALL=1, lists_vpn and whitelisted IPs to /usr/local/bin/pf-badhost.

/etc/pf.conf

ext_if = "vio0"

# Allow all on localhost
set skip on lo

# Block stateless traffic
block return

# Establish keep-state
pass

# Block all incoming by default
block in

# Block bad IPs
# https://www.geoghegan.ca/pfbadhost.html
#
# pfctl -t pfbadhost -T show
# pfctl -t pfbadhost -T flush
# pfctl -t pfbadhost -T add <IP>
# pfctl -t pfbadhost -T delete <IP>
# pfctl -t pfbadhost -T test <IP>
#
table <pfbadhost> persist file "/etc/pf-badhost.txt"
block in quick on $ext_if from <pfbadhost>
block out quick on $ext_if to <pfbadhost>

# Ban brute-force attackers
# http://home.nuug.no/~peter/pf/en/bruteforce.html
#
# pfctl -t bruteforce -T show
# pfctl -t bruteforce -T flush
# pfctl -t bruteforce -T delete <IP>
#
table <bruteforce> persist
block quick from <bruteforce>

# SSH
pass in on $ext_if inet proto tcp from any to $ext_if port 22 keep state (max-src-conn 15, max-src-conn-rate 5/3, overload <bruteforce> flush global)

# HTTP/HTTPS
pass in on $ext_if inet proto tcp from any to $ext_if port { 80, 443 } keep state

anchor "relayd/*"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment