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/443httpd(8)
listens for ACME challenges from Let's Encrypt on port HTTP/80 and passes them on toacme-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, whileunveil(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
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
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>';
root# pkg_add redis
root# rcctl enable redis
root# rcctl start redis
root# pkg_add node
root# npm install --global yarn
root# pkg_add sass
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
#!/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
require_relative "config/environment"
web: bundle exec falcon31 serve --threaded --bind http://0.0.0.0:6969
js: yarn build --watch
css: yarn build:css --watch
#!/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"
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
}
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"
}
}
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
}
#!/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
Install pf-badhost. Optionally add _TOR_BLOCK_ALL=1
, lists_vpn and whitelisted IPs to /usr/local/bin/pf-badhost
.
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/*"