![OpenBSD logo](https://gist.github.com/basicfeatures/329180cc42313466ae17795d2db6d510/raw/8ade3b328768486aac31c2792c1fe27e0bf37f10/openbsd_132x31.svg?sanitize=true)     ![Rails logo](https://gist.github.com/basicfeatures/99ed937ae501c42b61d35dcf916a7ee5/raw/a49a3de478b4cd957616ee69b4113d88f43d77a5/rails_119x40.svg?sanitize=true)     ![Falcon logo](https://gist.github.com/basicfeatures/ff1f7178181ccaf4588d00277e24a967/raw/5a61e9b05215b9de7c39ac5aba0847391bb7cfb2/falcon_139x49.svg?sanitize=true) > 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](https://www.openbsd.org/papers/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](https://rubyonrails.org/) and [Falcon](https://socketry.github.io/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](https://letsencrypt.com/) on port HTTP/80 and passes them on to `acme-client(1)` - `pf(4)` firewall locks down the system, and uses [pf-badhost](https://www.geoghegan.ca/pfbadhost.html) to block out roughly 600.000.000 spam IPs - Thanks to [ruby-pledge](https://github.com/jeremyevans/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 LOGIN SUPERUSER 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](https://github.com/libvips/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="" table { 127.0.0.1 } acme_client_port="23456" table { 127.0.0.1 } myappy_port="12345" http protocol "filter_challenge" { pass request path "/.well-known/acme-challenge/*" forward to } relay "http_relay" { listen on $egress port http protocol "filter_challenge" forward to 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 pass request header "Host" value "www.myappy.com" forward to tls keypair "myappy.com" # -- # Redis/Action Cable/StimulusReflex http websockets } relay "https_relay" { listen on $egress port https tls protocol "falcon" forward to 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](https://www.geoghegan.ca/pub/pf-badhost/latest/install/openbsd.txt). Optionally add `_TOR_BLOCK_ALL=1`, [lists_vpn](https://raw.githubusercontent.com/X4BNet/lists_vpn/main/ipv4.txt) 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 # pfctl -t pfbadhost -T delete # pfctl -t pfbadhost -T test # table persist file "/etc/pf-badhost.txt" block in quick on $ext_if from block out quick on $ext_if to # 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 # table persist block quick from # 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 flush global) # HTTP/HTTPS pass in on $ext_if inet proto tcp from any to $ext_if port { 80, 443 } keep state anchor "relayd/*" ```