Post

Building FreeBSD OVH VPS

This is based on 2016-11-06 Tim Chase blog page Installing FreeBSD on OVH. The method below is not using bsdinstall, but instead is based on bootstraping FreeBSD ZFS image from running system with make install(kernel,world,distribution). So it assumes that your FreeBSD world is already built and ready to be deployed from /usr/src and /usr/obj, in case it’s not, prepare it with Step 0 below.

Step 0 - Optional, building FreeBSD world

Create /etc/make.conf:

1
2
MK_PROFILE=no
WRKDIRPREFIX=/usr/obj

To build FreeBSD world from sources in /usr/src into /usr/obj, follow these steps, you will need git-lite package for this:

1
2
3
4
5
6
7
8
9
10
11
12
# make sure /usr/src and /usr/obj exist, best to have these as separate zfs datasets, so zfs create..
# first time only, if no sources yet, do
# git clone -b releng/14.2 https://git.freebsd.org/src.git /usr/src/

cd /usr/src
git pull -v
# if building FreeBSD 14.2 world
git checkout origin/releng/14.2

# use all cpu cores, will take few hours anyway, depending on your CPU, better do this in tmux
export NCPU=$(sysctl -n hw.ncpu)
make -j$NCPU buildworld && make -j$NCPU buildkernel

Step 1 - Prepare OVH VPS

I will target 5€/month OVH VLE-2 VPS, which comes with 40G disk, 2GB RAM and 2vcores. So, start by installing Linux OS from OVH VPS Dashboard, I tested Debian and CentOS, does not really matter, we will overwrite it with FreeBSD ZFS image. When installation is done, reboot VPS in recovery mode. OVH will send you an email with ssh access details, IP and password. Log-in, and type lsblk, there should be a block device sdb of size 40G. This is our dd output target in last Step 4.

Step 2 - Prepare FreeBSD configuration for VPS

Here are my FreeBSD configuration files for VPS that I keep in folder /root/ovh on my main FreeBSD host:

1
2
3
4
5
6
7
8
9
10
# ls -al /root/ovh/
total 48
-rw-r--r--  1 root wheel   78 Jul 21  2023 fstab
-rwxr-xr-x  1 root wheel 2772 Feb 18 12:53 go2.sh
-rw-r--r--  1 root wheel  107 Jul 21  2023 loader.conf
-r--r--r--  1 root wheel 2945 Nov 13  2022 localtime
-rw-r--r--  1 root wheel  370 Jul 21  2023 rc.conf
-rw-r--r--  1 root wheel   25 Nov 13  2022 resolv.conf
-rw-r--r--  1 root wheel 3843 Nov 13  2022 sshd_config
-rwx------  1 root wheel 2347 Feb 18 13:16 vps_image_build.sh

Let’s examine them:

fstab

1
2
# Device    Mountpoint  FStype  Options   Dump  Pass#
/dev/da0p2    none  swap  sw    0 0

go2.sh - shell script to create user account with wheel membership, add SSH access keys, and install some packages (I use doas, not sudo), also prepare /usr/local/etc/doas.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/sh

make_user() {
  NAME=$1
  ID=$2
  KEY=$3
  
  OUT=`pw usershow $NAME 2>/dev/null`
  if [ $? != 0 ]; then
    echo "creating user $NAME"
    pw groupadd -n $NAME -g $ID
    pw useradd  -n $NAME -u $ID -m -w no -M 750 -G wheel
  fi
  # always init .ssh dir and authkeys, even when user exists
  mkdir -p /home/$NAME/.ssh
  chmod 700 /home/$NAME/.ssh
  chown -R $NAME:$NAME /home/$NAME/.ssh
  printf "$KEY" > /home/$NAME/.ssh/authorized_keys
}

boot_repo() { # install some packages
  ASSUME_ALWAYS_YES=true pkg install doas bash joe node_exporter python311
}

prep_envi() {
  mkdir -p /usr/local/etc
  echo "
# Permit members of the wheel group to perform actions as root.
permit :wheel

permit nopass user1 as root
" >/usr/local/etc/doas.conf
  chmod 600 /usr/local/etc/doas.conf
}

set "user1" "10001" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDrlq+MKiQxeiPQFaX2jXLtoRnbuWDveVg0uvuoCkFgZ key1@host1\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxgZjma6sFSNb/u1YlaeiIiNnmUSgb8UA1NKj9w2e9l key2@host2\n"

make_user "$@"
boot_repo
prep_envi

loader.conf

1
2
3
4
kern.geom.label.disk_ident.enable="0"
kern.geom.label.gptid.enable="0"
cryptodev_load="YES"
zfs_load="YES"

localtime - copy this from my main host

1
cp /etc/localtime /root/ovh/

rc.conf - main OS startup configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
hostname=ovh-vps.uoga.net
zfs_enable="YES"
ifconfig_vtnet0=DHCP
dumpdev="AUTO"
ntpd_enable="YES"
sshd_enable="YES"
local_unbound_enable="YES"
local_unbound_tls="YES"

#pf_enable="YES"
#pflog_enable="YES"
#wireguard_enable="YES"
#wireguard_interfaces="wg0"

resolv.conf - resolver configuration, OVH nameserver by default

1
nameserver 213.186.33.99

sshd_config - optional, same as on my main host, so

1
cp /etc/ssh/sshd_config /root/ovh/

but then I add following part to the end of the file /root/ovh/sshd_config

1
2
3
4
5
6
7
8
9
10
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
Protocol 2
HostKey /etc/ssh/ssh_host_ed25519_key
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes

PrintMotd no
VersionAddendum AIX
X11Forwarding no

vps_image_build.sh - script to build VPS disk image. If you use memory disks, adjust the memorydisk number in mdconfig below to the first available on your host, I don’t use any, so I know mdconfig will create it as md0. The script will generate disk image, chroot into it to prompt for initial root password, then run user bootstrap script chrooted as /root/go2.sh to install some packages and your user account with SSH keys.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/bin/sh
mort() {
  echo "mort.." && exit
}

# variable containing ZFS pool name for OVH VPS, must be different from existing zpools on this host, or zpool create below will fail
export OVH_ZFS_POOL=rpoolovh
# folder with local assets, configuration files to include in ZFS image
export OVH_FOLDER=/root/ovh
# OVH VPS dd image file with ZFS partition
export OVH_IMAGE=/root/ovh/ovh.img
# OVH VPS image size, 2025 OVH VPS VLE-2 disk size is 40G
export OVH_DISK_SIZE=40G
# swap partition size, here 2G, so ZFS usable size will be 40G-2G=~38G
export OVH_SWAP_SIZE=2G

truncate -s $OVH_DISK_SIZE $OVH_IMAGE || mort
mdconfig -l  || mort
mdconfig -f $OVH_IMAGE -u 0  || mort
mdconfig -l  || mort
gpart create -s gpt md0  || mort
gpart add -t freebsd-boot -s 512K -i 1 md0  || mort
gpart add -t freebsd-swap -s $OVH_SWAP_SIZE -i 2 md0  || mort
gpart add -t freebsd-zfs -i 3 md0  || mort
gpart show md0  || mort
gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 md0  || mort
zpool create -R /mnt -O canmount=off -O mountpoint=/ -O atime=off -O compression=on ${OVH_ZFS_POOL} md0p3  || mort
zfs create -o mountpoint=none ${OVH_ZFS_POOL}/ROOT  || mort
zfs create -o mountpoint=/ ${OVH_ZFS_POOL}/ROOT/default  || mort
zpool set bootfs=${OVH_ZFS_POOL}/ROOT/default ${OVH_ZFS_POOL}  || mort
zfs create ${OVH_ZFS_POOL}/home  || mort
zfs create -o canmount=off ${OVH_ZFS_POOL}/usr  || mort
zfs create ${OVH_ZFS_POOL}/usr/obj  || mort
zfs create ${OVH_ZFS_POOL}/usr/src  || mort
zfs create ${OVH_ZFS_POOL}/jail  || mort
zfs create -o canmount=off ${OVH_ZFS_POOL}/var  || mort
zfs create ${OVH_ZFS_POOL}/var/log  || mort
zfs create ${OVH_ZFS_POOL}/var/tmp  || mort
zfs create ${OVH_ZFS_POOL}/tmp  || mort
cd /usr/src  || mort
make installkernel DESTDIR=/mnt  || mort
make installworld DESTDIR=/mnt  || mort
make distribution DESTDIR=/mnt  || mort
mkdir -p /mnt/usr/local/etc  || mort
cp $OVH_FOLDER/go2.sh /mnt/root/go2.sh  || mort
# use local resolver temporary, for chroot go2.sh inside image
cp /etc/resolv.conf /mnt/etc/
chroot /mnt passwd
chroot /mnt /root/go2.sh
cp $OVH_FOLDER/localtime $OVH_FOLDER/rc.conf $OVH_FOLDER/resolv.conf $OVH_FOLDER/fstab /mnt/etc/
cp $OVH_FOLDER/sshd_config /mnt/etc/ssh/
cp $OVH_FOLDER/loader.conf /mnt/boot/loader.conf
zpool export ${OVH_ZFS_POOL}
mdconfig -d -u 0
gzip --keep $OVH_IMAGE

Step 3 - execute /root/ovh/vps_image_build.sh to generate disk image

Below is the output of the script, output from make installkernel, make installworld removed between lines ... ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# /root/ovh/vps_image_build.sh 
md0 
md0 created
md0p1 added
md0p2 added
md0p3 added
=>      40  83886000  md0  GPT  (40G)
        40      1024    1  freebsd-boot  (512K)
      1064   4194304    2  freebsd-swap  (2.0G)
   4195368  79690672    3  freebsd-zfs  (38G)

partcode written to md0p1
bootcode written to md0
--------------------------------------------------------------
>>> Install check kernel
--------------------------------------------------------------
--------------------------------------------------------------
>>> Installing kernel GENERIC on Tue Feb 18 13:56:53 CET 2025
--------------------------------------------------------------
cd /usr/obj/usr/src/amd64.amd64/sys/GENERIC;  MACHINE_ARCH=amd64  MACHINE=amd64  CPUTYPE= CC="cc -target x86_64-unknown-freebsd14.2 --sysroot=/usr/obj/usr/src/amd64.amd64/tmp -B/usr/obj/usr/src/amd64.amd64/tmp/usr/bin" CXX="c++  -target x86_64-unknown-freebsd14.2 --sysroot=/usr/obj/usr/src/amd64.amd64/tmp -B/usr/obj/usr/src/amd64.amd64/tmp/usr/bin"  CPP="cpp -target x86_64-unknown-freebsd14.2 --sysroot=/usr/obj/usr/src/amd64.amd64/tmp -B/usr/obj/usr/src/amd64.amd64/tmp/usr/bin"  AS="as" AR="ar" ELFCTL="elfctl" LD="ld"  LLVM_LINK="" NM=nm OBJCOPY="objcopy"  RANLIB=ranlib STRINGS=  SIZE="size" STRIPBIN="strip" PATH=/usr/obj/usr/src/amd64.amd64/tmp/bin:/usr/obj/usr/src/amd64.amd64/tmp/usr/sbin:/usr/obj/usr/src/amd64.amd64/tmp/usr/bin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/usr/sbin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/usr/bin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/bin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/usr/libexec::/sbin:/bin:/usr/sbin:/usr/bin  make  KERNEL=kernel METALOG=  install
mkdir -p /mnt/boot/kernel
install -p -m 444 -o root -g wheel kernel /mnt/boot/kernel/
mkdir -p /mnt/usr/lib/debug/boot/kernel
...
...
===> etc (installconfig)
===> etc/termcap (installconfig)
installing DIRS CONFSDIR
install -N /usr/src/etc  -d -m 0755 -o root  -g wheel  /mnt/etc
install -N /usr/src/etc  -C -o root  -g wheel -m 644  termcap.small /mnt/etc/termcap.small
===> etc/sendmail (installconfig)
Changing local password for root
New Password:
Retype New Password:
creating user user1
Bootstrapping pkg from pkg+https://pkg.FreeBSD.org/FreeBSD:14:amd64/quarterly, please wait...
Verifying signature with trusted certificate pkg.freebsd.org.2013102301... done
Installing pkg-1.21.3...
Extracting pkg-1.21.3: 100%
Updating FreeBSD repository catalogue...
Fetching meta.conf: 100%    178 B   0.2kB/s    00:01    
Fetching data.pkg: 100%    7 MiB   2.5MB/s    00:03    
Processing entries: 100%
FreeBSD repository update completed. 35857 packages processed.
All repositories are up to date.
Updating database digests format: 100%
The following 12 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
  bash: 5.2.37
  doas: 6.3p12
  gettext-runtime: 0.23
  indexinfo: 0.3.1
  joe: 4.6_1,1
  libffi: 3.4.6
  mpdecimal: 4.0.0
  node_exporter: 1.8.2
  perl5: 5.36.3_2
  python311: 3.11.11
  readline: 8.2.13_2
  zfs-stats: 1.3.2

Number of packages to be installed: 12

The process will require 289 MiB more space.
49 MiB to be downloaded.
[1/12] Fetching indexinfo-0.3.1.pkg: 100%    6 KiB   5.9kB/s    00:01    
[2/12] Fetching mpdecimal-4.0.0.pkg: 100%  156 KiB 159.3kB/s    00:01    
[3/12] Fetching zfs-stats-1.3.2.pkg: 100%   10 KiB  10.6kB/s    00:01    
[4/12] Fetching node_exporter-1.8.2.pkg: 100%    4 MiB   3.9MB/s    00:01    
[5/12] Fetching perl5-5.36.3_2.pkg: 100%   15 MiB   1.8MB/s    00:09    
[6/12] Fetching joe-4.6_1,1.pkg: 100%  500 KiB 512.2kB/s    00:01    
[7/12] Fetching doas-6.3p12.pkg: 100%   24 KiB  24.6kB/s    00:01    
[8/12] Fetching libffi-3.4.6.pkg: 100%   45 KiB  46.0kB/s    00:01    
[9/12] Fetching readline-8.2.13_2.pkg: 100%  397 KiB 406.3kB/s    00:01    
[10/12] Fetching bash-5.2.37.pkg: 100%    2 MiB   1.8MB/s    00:01    
[11/12] Fetching gettext-runtime-0.23.pkg: 100%  236 KiB 241.2kB/s    00:01    
[12/12] Fetching python311-3.11.11.pkg: 100%   27 MiB   2.0MB/s    00:14    
Checking integrity... done (0 conflicting)
[1/12] Installing indexinfo-0.3.1...
[1/12] Extracting indexinfo-0.3.1: 100%
[2/12] Installing mpdecimal-4.0.0...
[2/12] Extracting mpdecimal-4.0.0: 100%
[3/12] Installing perl5-5.36.3_2...
[3/12] Extracting perl5-5.36.3_2: 100%
[4/12] Installing libffi-3.4.6...
[4/12] Extracting libffi-3.4.6: 100%
[5/12] Installing readline-8.2.13_2...
[5/12] Extracting readline-8.2.13_2: 100%
[6/12] Installing gettext-runtime-0.23...
[6/12] Extracting gettext-runtime-0.23: 100%
[7/12] Installing zfs-stats-1.3.2...
[7/12] Extracting zfs-stats-1.3.2: 100%
[8/12] Installing node_exporter-1.8.2...
[8/12] Extracting node_exporter-1.8.2: 100%
[9/12] Installing joe-4.6_1,1...
[9/12] Extracting joe-4.6_1,1: 100%
[10/12] Installing doas-6.3p12...
[10/12] Extracting doas-6.3p12: 100%
[11/12] Installing bash-5.2.37...
[11/12] Extracting bash-5.2.37: 100%
[12/12] Installing python311-3.11.11...
[12/12] Extracting python311-3.11.11: 100%
=====
Message from node_exporter-1.8.2:

--
If upgrading from a version of node_exporter <0.15.0 you'll need to update any
custom command line flags that you may have set as it now requires a
double-dash (--flag) instead of a single dash (-flag).
The collector flags in 0.15.0 have now been replaced with individual boolean
flags and the -collector.procfs` and -collector.sysfs` flags have been renamed
to --path.procfs and --path.sysfs respectively.
=====
Message from doas-6.3p12:

--
To use doas,

/usr/local/etc/doas.conf

must be created. Refer to doas.conf(5) for further details and/or follow
/usr/local/etc/doas.conf.sample as an example.

Note: In order to be able to run most desktop (GUI) applications, the user
needs to have the keepenv keyword specified. If keepenv is not specified then
key elements, like the user's $HOME variable, will be reset and cause the GUI
application to crash.

Users who only need to run command line applications can usually get away
without keepenv.

When in doubt, try to avoid using keepenv as it is less secure to have
environment variables passed to privileged users.
=====
Message from python311-3.11.11:

--
Note that some standard Python modules are provided as separate ports
as they require additional dependencies. They are available as:

py311-gdbm       databases/py-gdbm@py311
py311-sqlite3    databases/py-sqlite3@py311
py311-tkinter    x11-toolkits/py-tkinter@py311

Here are the contents of /root/ovh folder after script finished:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ls -al /root/ovh/
total 7101956
drwxr-xr-x  2 root wheel          12 Feb 18 13:59 .
drwxr-x---  7 root wheel          30 Feb 18 12:26 ..
-rw-r--r--  1 root wheel          78 Jul 21  2023 fstab
-rwxr-xr-x  1 root wheel        2772 Feb 18 12:53 go2.sh
-rw-r--r--  1 root wheel         107 Jul 21  2023 loader.conf
-r--r--r--  1 root wheel        2945 Nov 13  2022 localtime
-rw-r--r--  1 root wheel 42949672960 Feb 18 13:59 ovh.img
-rw-r--r--  1 root wheel  1655817216 Feb 18 13:59 ovh.img.gz
-rw-r--r--  1 root wheel         370 Jul 21  2023 rc.conf
-rw-r--r--  1 root wheel          25 Nov 13  2022 resolv.conf
-rw-r--r--  1 root wheel        3843 Nov 13  2022 sshd_config
-rwx------  1 root wheel        2347 Feb 18 13:16 vps_image_build.sh

Step 4 - write disk image to VPS

Replace X.X.X.X below with IP from OVH recovery mail, to restore VPS image via SSH:

1
ssh root@X.X.X.X "gunzip | dd of=/dev/sdb bs=1M" </root/ovh/ovh.img.gz

Reboot VPS from OVH Dashboard, and you should be able to login via SSH.

To later reimport pool image and temporarily mount it on /mnt on your main host:

1
mdconfig -f /root/ovh/ovh.img -u 0 && zpool import -R /mnt rpoolovh