DK1MI.radio

Hosting Gitea and FreshRSS on OpenBSD with httpd and relayd

This article tries to document the essential steps to achieve the following on a fresh OpenBSD 7.4 install:

The aim is to use built-in services such as httpd, relayd and acme instead of third party packages. You will not find any installation/configuration of database systems here, as I use SQLite for both Gitea and FreshRSS. The focus is on the server components, not the hosted applications, so I will not go into detail on how to configure them.

Packet Filter: pf and sshguard

Install sshguard:

$ pkg_add sshguard

Edit /etc/pf.conf, paste the following and adapt to your needs:

table <martians> {
  0.0.0.0/8 10.0.0.0/8 100.64.0.0/10            \
  127.0.0.0/8 169.254.0.0/16 172.16.0.0/12      \
  192.0.0.0/24 192.0.2.0/24 192.88.99.0/24      \
  192.168.0.0/16 198.18.0.0/15 198.51.100.0/24  \
  203.0.113.0/24 224.0.0.0/3 255.255.255.255/32 \
  ::/128 ::/96 ::1/128 ::ffff:0:0/96 100::/64   \
  2001:10::/28 2001:2::/48 2001:db8::/32        \
  3ffe::/16 fec0::/10 fc00::/7 }
 
## Set http(80)/https (443) port here ##
webports = "{http, https}"
 
## enable these services ##
int_tcp_services = "{domain, ntp, smtp, www, https, ftp, ssh, 31415}"
int_udp_services = "{domain, ntp}"

set block-policy drop
set loginterface egress
 
## Skip loop back interface - Skip all PF processing on interface ##
set skip on lo

match in all scrub (no-df random-id max-mss 1440)
match out on egress inet from !(egress:network) to any nat-to (egress:0)
 
## Blocking spoofed packets
antispoof quick for egress

# Drop all Non-Routable Addresses 
block in quick on egress from <martians> to any
block return out quick on egress from any to <martians>
 
## Set default policy ##
block all

table <sshguard> persist
block in quick proto tcp from <sshguard>
 
pass out quick
 
# Allow SSH
pass in inet proto tcp to egress port ssh
pass in inet6 proto tcp to egress port ssh

# Allow ICMP
pass in on egress inet proto icmp all icmp-type echoreq
pass in on egress inet6 proto icmp6 all icmp6-type echoreq

# Allow access to httpd and relayd
pass in inet proto { tcp udp } to egress port $webports
pass in inet6 proto { tcp udp } to egress port $webports
 
# Allow essential outgoing traffic 
pass out quick proto tcp to any port $int_tcp_services
pass out quick proto udp to any port $int_udp_services
pass out quick inet6 proto tcp to any port $int_tcp_services
pass out quick inet6 proto udp to any port $int_udp_services

Open a screen session and execute the following inside a dedicated screen:

$ sleep 120; pfctl -d

This allows us to reload the pf configuration without locking us out permanently. If make a mistake, pf will be disabled latest afer 2 minutes allowing us to ssh back in and fix the error.

$ pfctl -n -f /etc/pf.conf
$ pfctl -f /etc/pf.conf

Try to ssh in after reloading the config to make sure that at least this still works. If all is fine, don’t forget to kill the “sleep 120; pfctl -d” process.

Web Server: httpd and PHP

In this step we will configure httpd to serve static HTML as well as PHP files. It furthermore is needed for retrieving SSL certificates from Let’s Encrypt via ACME client.

Edit /etc/httpd, paste the following and adapt to your needs:

server "static.dk1mi.radio" {
   listen on * port 80
   root "/htdocs/static.dk1mi.radio"
   log style combined

   location "/.well-known/acme-challenge/*" {
      root "/acme"
      request strip 2
    }
}

server "blogs.radio" {
   listen on * port 80
   location "/*.php*" { fastcgi socket "/run/php-fpm.sock" }
   root "/freshrss/p/"
   directory index index.php
   log style combined

   location "/.well-known/acme-challenge/*" {
      root "/acme"
      request strip 2
   }
}

server "git.dk1mi.radio" {
   listen on * port 80
   log style combined

   location "/.well-known/acme-challenge/*" {
      root "/acme"
      request strip 2
   }

   location * {
       block return 301 "https://$HTTP_HOST$REQUEST_URI"
   }
}

Enable and start PHP:

$ rcctl enable php81_fpm
$ rcctl start php81_fpm

Enable and start httpd:

$ rcctl enable httpd
$ rcctl start httpd

Now it’s time to configure ACME to retrieve our SSL certificates:

Edit /etc/acme-client.conf, paste the following and adapt to your needs:

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

authority letsencrypt-staging {
        api url "https://acme-staging.api.letsencrypt.org/directory"
        account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

domain blogs.radio {
       domain key "/etc/ssl/private/blogs.radio.key"
       domain full chain certificate "/etc/ssl/blogs.radio.crt"
       sign with letsencrypt
}

domain git.dk1mi.radio {
       domain key "/etc/ssl/private/git.dk1mi.radio.key"
       domain full chain certificate "/etc/ssl/git.dk1mi.radio.crt"
       sign with letsencrypt
}

domain static.dk1mi.radio {
       domain key "/etc/ssl/private/static.dk1mi.radio.key"
       domain full chain certificate "/etc/ssl/static.dk1mi.radio.crt"
       sign with letsencrypt
}

We can now get our new certificates:

$ /usr/sbin/acme-client -v blogs.radio
$ /usr/sbin/acme-client -v git.dk1mi.radio
$ /usr/sbin/acme-client -v static.dk1mi.radio

In order to make sure that they will be updated regularly, we add the following lines to our crontab:

$ crontab -e
0 2 * * * /usr/sbin/acme-client -v beta.blogs.radio >> /tmp/acme.log 2>&1
5 2 * * * /usr/sbin/acme-client -v git.dk1mi.radio >> /tmp/acme.log 2>&1
10 2 * * * /usr/sbin/acme-client -v static.dk1mi.radio >> /tmp/acme.log 2>&1

After this has been done, we can now configure and start relayd.

Relay Daemon: relayd

We will make use of relayd to terminate SSL connections and forward HTTP requests internally to either httpd or the gitea daemon.

To do so, edit /etc/relayd, paste the following and adapt to your needs:

# Macros -----------------------------------
ext_ipv4="46.23.93.217"
ext_ipv6="2a03:6000:93f4:614::217"
webhost="127.0.0.1"
webhost6="::1"

table <webserver>  { $webhost }
table <gitea> { $webhost }
table <webserver6> { $webhost6 }

http protocol https {
  tls keypair git.dk1mi.radio
  tls keypair static.dk1mi.radio
  tls keypair blogs.radio

  block
  pass request header "Host" value "git.dk1mi.radio" \
    forward to <gitea>
  pass request header "Host" value "static.dk1mi.radio" \
    forward to <webserver>
  pass request header "Host" value "blogs.radio" \
    forward to <webserver>
}

relay https {
  listen on $ext_ipv4 port 443 tls
  protocol https
  forward to <webserver> port 80 mode roundrobin \
    check http "/" code 200
  forward to <gitea> port 3000
}

relay https6 {
  listen on $ext_ipv6 port 443 tls
  protocol https
  forward to <webserver6> port 80 mode roundrobin \
    check http "/" code 200
  forward to <gitea> port 3000
}

Enable and start relayd:

$ rcctl enable relayd
$ rcctl start relayd

FreshRSS

Install FreshRSS and some dependencies:

$ pkg_add freshrss php-zip php-curl

FreshRSS should now be accessible and configurable via your browser.

Add the following line to your crontab to automatically update all RSS feeds every 5th minute:

*/5 * * * * doas -u www /usr/local/bin/php -f /var/www/freshrss/app/actualize_script.php > /tmp/FreshRSS.log 2>&1

Gitea

$ pkg_add gitea

Enable and start gitea and gitdaemon:

$ rcctl enable gitea gitdaemon 
$ rcctl enable gitea gitdaemon 
$ rcctl start gitdaemon
$ rcctl start gitdaemon

Gitea should now be accessible and configurable via your browser.

Thank you for reading! If you have any comments or questions, please send me an e-mail.