Skip to content

Instantly share code, notes, and snippets.

@basicfeatures
Last active August 2, 2024 03:17
Show Gist options
  • Save basicfeatures/7d1448aad6fadc0def4ffee443f26dab to your computer and use it in GitHub Desktop.
Save basicfeatures/7d1448aad6fadc0def4ffee443f26dab 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

For 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