├── .gitignore ├── LICENSE ├── README-ARM64.md ├── README.md ├── bin └── hcloud ├── config.sh ├── etc └── rc.d │ └── hcloud └── util ├── patch.sh └── update.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 PaulC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-ARM64.md: -------------------------------------------------------------------------------- 1 | 2 | # Installing on ARM64 servers 3 | 4 | Hetzner Cloud now suports ARM64 (AARCH64) servers (currently only in 5 | Falkenstein DC) however it isn't currently possible to install FreeBSD directly 6 | from CDROM (the system boots but there is no video output in the EFI console), 7 | and (afaik) it also doesn't currently appear to be possibe to boot MfsBSD on 8 | Arm64/EFI. 9 | 10 | There are however a couple of other options we can use (both use the Linux 11 | rescue system). 12 | 13 | # FreeBSD VM image 14 | 15 | We can use the official FreeBSD VM image to install the system - however we do 16 | need to patch this before writing to thw VM disc (see 17 | https://gist.github.com/pandrewhk/2d62664bfb74a504b7f4a894fc85eb97) 18 | 19 | To patch the image you will need an existing FreeBSD host (any architecture). 20 | 21 | a. On the existing FreeBSD host download the appropriate FreeBSD raw VM image 22 | 23 | curl https://download.freebsd.org/releases/VM-IMAGES/13.2-RELEASE/aarch64/Latest/FreeBSD-13.2-RELEASE-arm64-aarch64.raw.xz) 24 | 25 | b. Mount the image as a loopback device 26 | 27 | unxz FreeBSD-13.2-RELEASE-arm64-aarch64.raw.xz 28 | mdconfig -u1 FreeBSD-13.2-RELEASE-amd64.raw 29 | mount /dev/md1p4 /mnt 30 | printf 'sshd_enable="YES"\nsshd_flags="-o PermitRootLogin=yes"\ndevmatch_blacklist="virtio_random.ko"\n' | tee -a /mnt/etc/rc.conf 31 | umask 077 32 | mkdir /mnt/root/.ssh 33 | echo "${SSH_PUB_KEY?}" > /mnt/root/.ssh/authorized_keys 34 | 35 | (Note: you need to set the SSH_PUB_KEY env var to your ssh public key - eg. contents of .ssh/id_ed25519.pub) 36 | 37 | c. Make any other necessary changes to the base image (eg. growfs_enable="NO" if you want to add a ZFS partition instead of expanding the UFS partition)) 38 | 39 | d. Unmount the image and recompress 40 | 41 | umount /mnt 42 | mdconfig -d -u 0 43 | xz FreeBSD-13.2-RELEASE-amd64.raw 44 | 45 | e. Make sure that the image available (http/ftp) - (eg. python3 -m http.server) 46 | 47 | f. Boot the Hetzner ARM64 server into rescue mode and connect via SSH 48 | 49 | g. Download and write the image directly to the VM disc 50 | 51 | curl http://.... | unxz > /dev/sda 52 | 53 | h. Reboot and connect to the server using SSH 54 | 55 | i. Remove buggy virtio_random driver 56 | 57 | sysrc devmatch_blacklist="virtio_random.ko" # Avoid virtio_random.ko bug 58 | 59 | j. Follow normal installation instructions in config.sh 60 | 61 | fetch -o /tmp/config.sh https://raw.githubusercontent.com/paulc/hcloud-freebsd/master/config.sh 62 | sh -v /tmp/config.sh 63 | 64 | #### Note: another option is to just install the distribution image directly onto the disc and then use the QEMU option below (dont attach ISO drive) to configure the system (this avoids having to patch the image first) 65 | 66 | # QEMU install 67 | 68 | #### _Note: This method doesnt boot with UFS install (looks like generated FS is corrupt but difficult to diagnose as console doesnt work) - for a UFS install you currently need to use the FreeBSD VM image option (see above). (ZFS install appears to work fine)_ 69 | 70 | The QEMU install option is a bit more complex however does allow you to customise the install (in particular if you want to install on ZFS root) 71 | 72 | a. Boot the VM into rescue mode and connect using SSH 73 | 74 | b. Install qemu-system-arm 75 | 76 | apt install -y qemu-system-arm qemu-efi-aarch64 77 | 78 | c. Download ARM installer 79 | 80 | curl -Lo freebsd.iso https://download.freebsd.org/releases/arm64/aarch64/ISO-IMAGES/13.2/FreeBSD-13.2-RELEASE-arm64-aarch64-bootonly.iso 81 | 82 | or 83 | 84 | curl -Lo freebsd.iso https://download.freebsd.org/releases/arm64/aarch64/ISO-IMAGES/13.2/FreeBSD-13.2-RELEASE-arm64-aarch64-disc1.iso 85 | 86 | d. Create EFI flash images 87 | 88 | dd if=/dev/zero of=efi.img bs=1M count=64 89 | dd if=/dev/zero of=efi-varstore.img bs=1M count=64 90 | dd if=/usr/share/qemu-efi-aarch64/QEMU_EFI.fd of=efi.img conv=notrunc 91 | 92 | e. Boot installer from QEMU 93 | 94 | qemu-system-aarch64 \ 95 | -machine virt,gic-version=max \ 96 | -nographic \ 97 | -m 1024M \ 98 | -cpu max \ 99 | -device virtio-net-pci,netdev=nic \ 100 | -netdev user,id=nic,hostfwd=tcp:127.0.0.1:2022-:22 \ # Only really needed for booting live image 101 | -drive file=efi.img,format=raw,if=pflash \ 102 | -drive file=efi-varstore.img,format=raw,if=pflash \ 103 | -drive file=/dev/sda,format=raw,if=none,id=drive0,cache=writeback \ 104 | -device virtio-blk,drive=drive0 \ 105 | -drive file=freebsd.iso,if=none,id=drive1,cache=writeback \ 106 | -device virtio-blk,drive=drive1,bootindex=0 107 | 108 | f. Follow installer prompts as normal - when done drop into shell 109 | 110 | sysrc devmatch_blacklist="virtio_random.ko" # Avoid virtio_random.ko bug 111 | 112 | g. Follow normal installation instructions in config.sh 113 | 114 | fetch -o /tmp/config.sh https://raw.githubusercontent.com/paulc/hcloud-freebsd/master/config.sh 115 | sh -v /tmp/config.sh 116 | 117 | h. Shutdown and exit QEMU (C-a x) 118 | 119 | i. Smapshot the instance 120 | 121 | # Rescue system 122 | 123 | You can also use QEMU as a rescue system (use the Live CD rather than Installer 124 | option when the ISO boots) or to boot the VM directly (remove the ISO 125 | device/drive). 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hcloud-freebsd 2 | 3 | Hetzner Cloud auto-provisioning for FreeBSD 4 | 5 | ## Introduction 6 | 7 | This repository enables auto-provisioning of FreeBSD instances on 8 | [Hetzner Cloud](https://www.hetzner.com/cloud). 9 | 10 | Currently only Linux auto-provisioning is enabled by default however by 11 | initially manually configuring a FreeBSD instance and adding the `hcloud` 12 | utility and `rc.d` script included in this repository, it is possible to create 13 | a snapshot which can be used as a base instance and supports the normal 14 | auto-configuration functions available either in the cloud console or via the 15 | api/cli tools. 16 | 17 | _Note that currently FreeBSD 12.X doesn't boot on CPX (AMD/EPYC) instances - only CP (XEON). FreeBSD 13.X DOES boot however._ 18 | 19 | ## Installation 20 | 21 | ### OS Installation 22 | 23 | #### To install FreeBSD 13 on ARM64 machines (cax__) see [README-ARM64.md](./README-ARM64.md). 24 | #### Note: This isnt necessary for FreeBSD 14 (ISO is available from ISO menu and normal installation works fine) 25 | 26 | Automated installation of FreeBSD instances is not currently available for 27 | Hetzner Cloud, however it is possible to manually configure an instance as 28 | follows: 29 | 30 | * Create a VM instance using the [cloud console](https://console.hetzner.cloud/projects). 31 | Pick a server type that matches the one you want to provision as a template 32 | (usually the smallest SSD type - currently CX11 - as you can resize instances 33 | upwards). The base image doesn't matter at this stage. 34 | 35 | * When the server has booted select the instance in the cloud console and 36 | attach a FreeBSD ISO image (select _ISO Images_ and search for an appropriate 37 | FreeBSD instance). The script will also support HardenedBSD however you will 38 | need to ask support to make the ISO available. 39 | 40 | * From the cloud console open the device console (**>_**) and reboot server. 41 | 42 | * The FreeBSD installer should now start and you can install FreeBSD as normal. 43 | See the [FreeBSD handbook](https://www.freebsd.org/doc/handbook/bsdinstall.html) for details. 44 | The recommended options for installation are: 45 | 46 | - Appropriate keymap/hostname 47 | - Default install components (kernel-dbg/lib32) 48 | - Configure networking (**vtnet0/IPv4/DHCP**) - don't worry about configuring 49 | IPv6 at the moment (will be configured for cloned instances through cloud-config) 50 | - Select distrobution mirror - default is fine (ftp://ftp.freebsd.org) 51 | - Select **Auto (UFS)** partition type, **Entire Disk**, **GPT**, and accept default partitions (it is also possible to use ZFS if prefered - though UFS might be more suitable for low-memory instances). 52 | - _(Distribution files should now install)_ 53 | - Set root password (this is only needed for initial configuration - password login will be 54 | disabled for instances) 55 | - Select appropriate Time Zone and Date/Time 56 | - Select default services (at least **sshd**) 57 | - Chose security hardening options (I usually select all of these) 58 | - Do **not** add users to the system unless you specifically want these as part of the base image 59 | - Exit installer making shre you select **Yes** to drop to **shell** to complete configuration 60 | 61 | * From the installation shell follow the instructions in [config.sh](https://github.com/paulc/hcloud-freebsd/blob/master/config.sh) (either manually or by downloading the script): 62 | 63 | * The instance will power off at the end of the installation 64 | 65 | * From the Hetzner cloud console 66 | - **Unmount ISO** 67 | - From Snapshots menu **Take Snapshot** 68 | - When the snapshot has been created you can now use this as a template to 69 | start new cloud instances 70 | 71 | ### Creating Instances 72 | 73 | * To create a new instance click on **Add Server** as normal and select the 74 | appropriate snapshot from the **Images / Snapshots** tab (you can also 75 | view the the snapshot page and create a new server from there). 76 | 77 | * Select the options as normal on the **Add Server** page. These will be picked up by 78 | the `rc/hcloud` script on firstboot and the server configured. 79 | 80 | * The script supports auto-configuration of the following settings: 81 | 82 | - **hostname** 83 | - **network interfaces** (iprimary interface IPv4 and IPv6 addresses, 84 | additional private interfaces will be autodetected and configured to run 85 | DHCP) 86 | - **ssh keys** will be added to root user 87 | - **userdata** script will be run. Note that the userdata script will be 88 | written to disk and run directly so must be a valid script for the 89 | target system - in particular you will almost certainly just want to 90 | use a plain /bin/sh script (first line should be `#!/bin/sh`). Multipart 91 | files and cloud-config (`#cloud-config`) data are not supported (GZ 92 | compressed files _are_ supported). 93 | 94 | * Note that additional volumes are not auto-configured but will be 95 | automatically detected by the kernel (`/dev/da[123...]`) so could be 96 | configured/mounted using the user-data script. 97 | 98 | * If needed it is possible to grow the FS for larger instances automatically 99 | via the **userdata** script. It should also be possible to use `rc.d/growfs` 100 | (needs `growfs_enable=YES` in `rc.conf` and the root partition to be the last 101 | partition) although I haven't tested this. 102 | 103 | * Alternatively it is posisble to use the additional space to add an additional 104 | FS (eg. for ZFS) from the userdata script. You can check if the image has 105 | been installed onto a larger sized instance by running `gpart show da0 | grep 106 | -qs CORRUPT` and then `gpart recover` / `gpart add` etc. 107 | 108 | * A copy of the cloud configuration parameters (split by section), the 109 | user-data script, and an installation log are saved in the /var/hcloud 110 | directory. 111 | 112 | * The rc(8) system will automatically delete the /firstboot flag after 113 | the first-boot so the script will only run once. 114 | 115 | * It is also possible to configure new instances via the API or hcloud 116 | utility - eg: 117 | 118 | - `hcloud server create --image --name --user-data-from-file --ssh-key --type --location ` 119 | 120 | ### Maintaining Images 121 | 122 | * To maintain images (run freebsd-update/update pkgs etc) a couple of 123 | example scripts are provides in the **/util** directory. 124 | 125 | - [update.sh](https://github.com/paulc/hcloud-freebsd/blob/master/util/update.sh) 126 | will automatically run basic OS/pkg updates on the image and then resave 127 | (deleting original) 128 | 129 | - [patch.sh](https://github.com/paulc/hcloud-freebsd/blob/master/util/patch.sh) 130 | will do the same but first launch a single use sshd instance on port 9022 131 | to allow interactive configuration 132 | 133 | - (Note that in both cases the **imageid** will change) 134 | -------------------------------------------------------------------------------- /bin/hcloud: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | 3 | import email,gzip,json,pathlib,subprocess,sys,urllib.request,urllib.error 4 | import yaml 5 | 6 | def sysrc(key,val=None): 7 | # Read/write rc.conf values using sysrc 8 | if val: 9 | subprocess.run(['/usr/sbin/sysrc','{}={}'.format(key,val)],check=True) 10 | else: 11 | subprocess.run(['/usr/sbin/sysrc',key],check=True) 12 | 13 | def runrc(name,check=True,method='restart',args=None): 14 | # Rerun service initialisation (picking up new rc.conf values) 15 | subprocess.run(['/usr/sbin/service',name,method] + (args or []),check=check) 16 | 17 | def savejson(name,data): 18 | # Save section as JSON 19 | with open(name,'w') as f: 20 | json.dump(data,f,indent=4) 21 | f.write("\n") 22 | 23 | def msg(s): 24 | print("\n{}\n".format(s)) 25 | sys.stdout.flush() 26 | 27 | def vendor_data(data): 28 | # Handle vendor_data section - we can ignore this but save anyway 29 | # Extract multipart-mime files ('hc-boot-script','cloud-config') 30 | vendor_data = email.message_from_string(data) 31 | for p in vendor_data.walk(): 32 | if not p.is_multipart(): 33 | name = p.get_filename() 34 | with open(name,"w") as f: 35 | f.write(p.get_payload()) 36 | f.write("\n") 37 | 38 | def hostname(name): 39 | # Set hostname 40 | msg("[+] Setting hostname") 41 | sysrc('hostname',name) 42 | runrc('hostname') 43 | 44 | def sshkeys(keys): 45 | # Write SSH keys to /root/.ssh/authorized_keys 46 | msg("[+] Adding SSH keys") 47 | sshdir = pathlib.Path('/root/.ssh') 48 | sshdir.mkdir(mode=0o700,exist_ok=True) 49 | ak = sshdir / 'authorized_keys' 50 | with ak.open('w') as f: 51 | for sshkey in keys: 52 | f.write(sshkey) 53 | f.write('\n') 54 | ak.chmod(0o600) 55 | 56 | def network_config(config): 57 | # You might expect all network interfaces to be defined here 58 | # but only primary interface data is provided (does not include 59 | # private interfaces) - though we go through list anyway 60 | msg("[+] Configuring network") 61 | for iface in config: 62 | # Rename interface from ethXX to vtnetXX 63 | ifname = iface['name'].replace('eth','vtnet') 64 | for subnet in iface['subnets']: 65 | if subnet.get('ipv4',False): 66 | # We always configure primary interface IPv4 via DHCP 67 | if subnet['type'] == 'dhcp': 68 | sysrc('ifconfig_{}'.format(ifname),'DHCP') 69 | elif subnet.get('ipv6',False): 70 | # Configure static IPv6 address 71 | if subnet['type'] == 'static': 72 | address,prefix = subnet['address'].split('/') 73 | sysrc('ifconfig_{}_ipv6'.format(ifname), 74 | 'inet6 {} prefixlen {}'.format(address,prefix)) 75 | if subnet.get('gateway',False): 76 | # Occasionally we dont get a correct link-local address 77 | if subnet['gateway'].startswith('fe80:'): 78 | gw_addr = subnet['gateway'].split('%')[0] + '%' + ifname 79 | sysrc('ipv6_defaultrouter', gw_addr) 80 | else: 81 | sysrc('ipv6_defaultrouter', subnet['gateway']) 82 | # We now configure any unconfigured interfaces 83 | ifaces = subprocess.run(['/sbin/ifconfig','-ld','ether'], 84 | capture_output=True,check=True) 85 | for ifname in ifaces.stdout.decode('ascii').split(): 86 | sysrc('ifconfig_{}'.format(ifname),'DHCP') 87 | # We now reconfigure network interfaces and routing 88 | msg("[+] Restarting network") 89 | runrc('netif') 90 | runrc('routing',False) # Ignore errors from existing routes 91 | runrc('dhclient',args=['vtnet0']) 92 | 93 | def hcloud_metadata(): 94 | # Get instance metadata 95 | try: 96 | with urllib.request.urlopen('http://169.254.169.254/hetzner/v1/metadata') as r: 97 | # Parse YAML 98 | config = yaml.safe_load(r.read()) 99 | except urllib.error.URLError as e: 100 | raise ValueError("Error fetching instance metadata: {}".format(e.reason)) 101 | 102 | # Handle sections 103 | for k,v in config.items(): 104 | if k == "vendor_data": 105 | vendor_data(v) 106 | else: 107 | # Save config section to local directory (usually /var/hcloud) 108 | savejson(k,v) 109 | if k == 'hostname': 110 | hostname(v) 111 | elif k == 'public-keys': 112 | sshkeys(v) 113 | elif k == 'network-config': 114 | network_config(v['config']) 115 | 116 | def hcloud_userdata(): 117 | # Get instance userdata 118 | try: 119 | msg("[+] Running userdata script") 120 | with urllib.request.urlopen('http://169.254.169.254/hetzner/v1/userdata') as r: 121 | # Write to 'user-data' 122 | userdata = pathlib.Path('./user-data') 123 | with userdata.open('wb') as f: 124 | # Check for GZ compressed 125 | if r.peek()[:3] == b'\x1f\x8b\x08': 126 | f.write(gzip.decompress(r.read())) 127 | else: 128 | f.write(r.read()) 129 | userdata.chmod(0o700) 130 | if userdata.stat().st_size > 0: 131 | subprocess.run(['./user-data'],check=True) 132 | except urllib.error.URLError as e: 133 | raise ValueError("Error fetching instance userdata: {}".format(e.reason)) 134 | 135 | if __name__ == '__main__': 136 | # Get metadata 137 | hcloud_metadata() 138 | # Try to get fqdn from cloud-config 139 | try: 140 | with open("/var/hcloud/cloud-config") as f: 141 | cloud_config = yaml.safe_load(f) 142 | fqdn = cloud_config.get("fqdn") 143 | if fqdn: 144 | hostname(fqdn) 145 | except: 146 | pass 147 | # Get userdate 148 | hcloud_userdata() 149 | -------------------------------------------------------------------------------- /config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # hcloud-freebsd/config.sh 4 | # 5 | # This script configures a clean FreeBSD install to support Hetzner cloud 6 | # auto-provisioning. 7 | # 8 | # You can either run the commands manually or setup the system automatically 9 | # by downloading the this script (a git.io short link is available). 10 | # 11 | # fetch -o config.sh https://github.com/paulc/hcloud-freebsd/raw/master/config.sh 12 | # sh ./config.sh 13 | # 14 | # (Note that the CA root cert bundle is now installed by default) 15 | # 16 | # The system will be powered off once the script has run and you should then 17 | # detach the ISO image and snapshot the instance (which can then be used as 18 | # a template) 19 | 20 | set -e 21 | 22 | # Update system 23 | if which hbsd-update; then # HardenedBSD support 24 | hbsd-update 25 | else 26 | freebsd-update fetch --not-running-from-cron | cat 27 | freebsd-update install --not-running-from-cron || echo "No updates available" 28 | fi 29 | 30 | # Bootstrap pkg tool and install required packages 31 | ASSUME_ALWAYS_YES=yes pkg bootstrap 32 | pkg update 33 | 34 | # Get pkgs 35 | pkg install -y ca_root_nss python3 $(pkg search -q -S name '^py3[0-9]+-yaml$' | sort | tail -1) 36 | 37 | # Install hcloud utility 38 | mkdir -p /usr/local/bin 39 | fetch -o /usr/local/bin/hcloud https://raw.githubusercontent.com/paulc/hcloud-freebsd/master/bin/hcloud 40 | chmod 755 /usr/local/bin/hcloud 41 | 42 | # Install hcloud rc script 43 | mkdir -p /usr/local/etc/rc.d 44 | fetch -o /usr/local/etc/rc.d/hcloud https://raw.githubusercontent.com/paulc/hcloud-freebsd/master/etc/rc.d/hcloud 45 | chmod 755 /usr/local/etc/rc.d/hcloud 46 | 47 | # Enable hcloud service 48 | sysrc hcloud_enable=YES 49 | 50 | # Allow root login with SSH key 51 | sysrc sshd_enable="YES" 52 | sysrc sshd_flags="-o AuthenticationMethods=publickey -o PermitRootLogin=prohibit-password" 53 | 54 | # Clear tmp 55 | sysrc clear_tmp_enable="YES" 56 | 57 | # Disable sendmail and remote syslogd socket 58 | sysrc syslogd_flags="-ss" 59 | sysrc sendmail_enable="NONE" 60 | 61 | # Sync time on boot 62 | sysrc ntpdate_enable="YES" 63 | 64 | # Expand root device on boot 65 | sysrc growfs_enable=YES 66 | 67 | # Clean keys/logs 68 | rm -f /etc/ssh/*key* 69 | rm -f /root/.ssh/authorized_keys 70 | truncate -s0 /var/log/* 71 | 72 | # Set root shell to /bin/sh 73 | pw usermod root -s /bin/sh 74 | 75 | # Disable root password login 76 | pw usermod root -h - 77 | 78 | # Create /firstboot flag for rc(8) 79 | touch /firstboot 80 | 81 | # Poweroff machine 82 | echo "Configuration completed - detach ISO and create snapshot." 83 | read -p "Do you want to power-off instance? [yn]: " yn 84 | case $yn in 85 | [Yy]*) shutdown -p now; 86 | break;; 87 | *) ;; 88 | esac 89 | 90 | -------------------------------------------------------------------------------- /etc/rc.d/hcloud: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # PROVIDE: hcloud 4 | # REQUIRE: NETWORKING 5 | # BEFORE: DAEMON 6 | # KEYWORD: firstboot 7 | 8 | # This rc script enables Hetzner Cloud auto-configuration 9 | # at firstboot (https://github.com/paulc/hcloud-freebsd) 10 | 11 | . /etc/rc.subr 12 | 13 | name="hcloud" 14 | desc="Auto-configure Hetzner Cloud instances" 15 | start_cmd="hcloud_start" 16 | stop_cmd=":" 17 | rcvar="hcloud_enable" 18 | 19 | hcloud_start() { 20 | echo "Staring Hetzner cloud-config: " 21 | # Ensure that we clear SSHD host keys 22 | rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub 23 | # Ensure /var/hcloud directory is clear 24 | rm -rf /var/hcloud 25 | mkdir -m 700 /var/hcloud 26 | # Change directory to /var/hcloud 27 | cd /var/hcloud 28 | # Run cloud-config 29 | /usr/local/bin/hcloud 2>&1 | tee hcloud.$(date +%Y%m%d-%H%M%S).log 30 | echo "cloud-config done" 31 | } 32 | 33 | load_rc_config $name 34 | run_rc_command "$1" 35 | -------------------------------------------------------------------------------- /util/patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Manually patch and update FreeBSD image (starts single use ssh server 4 | # on port 9022 to allow interactive update of image and then runs 5 | # freebsd-update/pkg update and cleans up runtime artefacts) 6 | #  7 | # Requires hcloud cli (https://github.com/hetznercloud/cli) to be 8 | # installed 9 | # 10 | # Usage: 11 | # 12 | # IMAGE= SSHKEY= ./patch.sh 13 | # (In a separate session ssh to instance port 9022) 14 | # 15 | # By deefault this will create a cx11 image in fsn1 - to change set 16 | # LOCATION/TYPE environment variables 17 | #  18 | # (Note that the original image will be deleted) 19 | # 20 | 21 | set -o pipefail 22 | set -o errexit 23 | 24 | : ${LOCATION:=fsn1} 25 | : ${TYPE:=cx11} 26 | 27 | if [ -z "${IMAGE}" ]; then 28 | echo "\nERROR: Must specify IMAGE\n" 29 | hcloud image list --type snapshot 30 | exit 1 31 | fi 32 | 33 | if [ -z "${SSHKEY}" ]; then 34 | echo "\nERROR: Must specify SSHKEY\n" 35 | hcloud ssh-key list 36 | exit 1 37 | fi 38 | 39 | set -o nounset 40 | 41 | TS=$(date +%Y%m%d-%H%M%S) 42 | NAME="update-${TS}" 43 | DESCRIPTION=$(hcloud image describe -o format='{{.Description}}' ${IMAGE}) 44 | BASE_DESCRIPTION=$(echo $DESCRIPTION | sed -Ee 's/-[0-9]{8}-[0-9]{6}$//') 45 | 46 | echo "+++ When server starts SSH to port 9022 and update system:" 47 | echo "+++ (Make sure you're not keeping a persistent session open (-o ControlPersist=no)" 48 | 49 | 50 | hcloud server create --location ${LOCATION} --type ${TYPE} --image ${IMAGE} --name ${NAME} --ssh-key ${SSHKEY} --user-data-from-file - <<'EOM' 51 | #!/bin/sh 52 | ( service sshd onekeygen 53 | /usr/sbin/sshd -d -o Port=9022 -o PermitRootLogin=prohibit-password 54 | freebsd-update fetch --not-running-from-cron | head 55 | freebsd-update install --not-running-from-cron || echo No updates available 56 | pkg update 57 | pkg upgrade -y 58 | rm -f /var/hcloud/* 59 | rm -f /etc/ssh/*key* 60 | rm -f /root/.ssh/authorized_keys 61 | truncate -s0 /var/log/* 62 | sysrc -x ifconfig_vtnet0_ipv6 ipv6_defaultrouter 63 | touch /firstboot 64 | shutdown -p now ) 2>&1 | tee /var/log/update-$(date +%Y%m%d-%H%M%S).log 65 | EOM 66 | 67 | printf "Waiting for server shutdown" 68 | 69 | while [ "$(hcloud server describe -o format='{{.Status}}' $NAME)" != "off" ]; do 70 | printf "." 71 | sleep 1 72 | done 73 | 74 | printf "\n" 75 | 76 | hcloud server create-image --description "${BASE_DESCRIPTION}-${TS}" --type snapshot ${NAME} 77 | 78 | hcloud server delete ${NAME} 79 | 80 | hcloud image delete ${IMAGE} 81 | -------------------------------------------------------------------------------- /util/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Update FreeBSD image (runs freebsd-update & pkg update/upgrade 4 | # and cleans up runtime artefacts) 5 | #  6 | # Requires hcloud cli (https://github.com/hetznercloud/cli) to be 7 | # installed 8 | # 9 | # Usage: 10 | # 11 | # IMAGE= ./update.sh 12 | # 13 | # By deefault this will create a cx11 image in fsn1 - to change set 14 | # LOCATION/TYPE environment variables 15 | #  16 | # (Note that the original image will be deleted) 17 | # 18 | 19 | set -o pipefail 20 | set -o errexit 21 | 22 | : ${LOCATION:=fsn1} 23 | : ${TYPE:=cx11} 24 | 25 | if [ -z "${IMAGE}" ]; then 26 | echo "\nERROR: Must specify IMAGE\n" 27 | hcloud image list --type snapshot 28 | exit 1 29 | fi 30 | 31 | set -o nounset 32 | 33 | TS=$(date +%Y%m%d-%H%M%S) 34 | NAME="update-${TS}" 35 | DESCRIPTION=$(hcloud image describe -o format='{{.Description}}' ${IMAGE}) 36 | BASE_DESCRIPTION=$(echo $DESCRIPTION | sed -Ee 's/-[0-9]{8}-[0-9]{6}$//') 37 | 38 | hcloud server create --location ${LOCATION} --type ${TYPE} --image ${IMAGE} --name ${NAME} --user-data-from-file - <<'EOM' 39 | #!/bin/sh 40 | ( freebsd-update fetch --not-running-from-cron | head 41 | freebsd-update install --not-running-from-cron || echo No updates available 42 | pkg update 43 | pkg upgrade -y 44 | rm -f /var/hcloud/* 45 | rm -f /etc/ssh/*key* 46 | rm -f /root/.ssh/authorized_keys 47 | truncate -s0 /var/log/* 48 | sysrc -x ifconfig_vtnet0_ipv6 ipv6_defaultrouter 49 | touch /firstboot 50 | shutdown -p now ) 2>&1 | tee /var/log/update-$(date +%Y%m%d-%H%M%S).log 51 | EOM 52 | 53 | printf "Waiting for server shutdown" 54 | 55 | while [ $(hcloud server describe -o format='{{.Status}}' $NAME) != "off" ]; do 56 | printf "." 57 | sleep 1 58 | done 59 | 60 | printf "\n" 61 | 62 | hcloud server create-image --description "${BASE_DESCRIPTION}-${TS}" --type snapshot ${NAME} 63 | 64 | hcloud server delete ${NAME} 65 | 66 | hcloud image delete ${IMAGE} 67 | --------------------------------------------------------------------------------