Adding a bastion host - setting up the bastion
After finally breaking through the barrier of migrating sufficient of my SaltStack managed configuration to Ansible, I can build my first bastion host. My thinking has moved on slightly from my original design but, for now, I am going to try and focus this post on just building the bastion and leave discussing the network design for another post.
Following the recent SSH logging issue, dubbed regreSSHion (in light of it being a reintroduced vulnerability) I decided to use OpenBSD for the bastion because of:
OpenBSD systems are unaffected by this bug, as OpenBSD developed a secure mechanism in 2001 that prevents this vulnerability.
This also adds an element of defence-in-depth, since most of my systems are running Linux forcing traversal of a BSD system between security zones means that a Linux specific vulnerability would be confined to being exploited within one zone - likewise, a BSD specific vulnerability could allow compromise of the bastion but would not allow compromise of the non-BSD systems behind that host.
I decided (this is part of my rethinking) to create a new “demilitarized zone (DMZ)” VLAN on my switch and use switch ACLs to control access in and out of the DMZ. The OpenBSD host will exist inside this DMZ, as well as a front-end proxy to present some services out to the wider network.
Switch VLAN and routing configuration
Adding the DMZ vlan:
enable
configure
vlan 11
name Management-DMZ
exit
Add DMZ vlan trunk to Proxmox development port (as bastion will be a VM on the cluster - it’s already a trunk so just need to allow it):
interface gigabitEthernet 1/0/47
switchport general allowed vlan 11 tagged
Set the switch up to route between the networks:
interface vlan 11
ip address 192.168.11.250 255.255.255.0
exit
interface vlan 10
ip address 192.168.10.251 255.255.255.0
exit
interface vlan 20
ip address 192.168.20.251 255.255.255.0
exit
VM setup
I added the VM to the inventory:
proxmox_vms:
hosts:
# ...
k7-mgmt:
proxmox_vm:
vlan: 11
Run the create-vms.yaml
playbook:
ansible-playbook -i inventory.yaml create-vms.yaml
I downloaded an OpenBSD ISO and installed it on the VM manually. Key configurations:
- manually configuring an IP address of
192.168.11.100
- default route
none
none
DNS server- Setup a user for me
no
to allow root ssh loginsd0
root disk- Encrypt root disk with passphrase
- Installed filesets:
bsd
,base75.tgz
andman75.tgz
- timezone:
Europe/London
Switch ACL configuration
I added a (temporary) route to my current central router, so existing systems will be able to go via that route, using the main network interface on the switch:
ip route add 192.168.11.0/24 via 192.168.20.251
I then attempted to ping it from the router which did not work, the switch replied “Destination Host Unreachable”. To troubleshoot, I logged into the switch and checked the routing table (show ip route
), which appeared correct, and tried to ping from the switch, which failed. I then tried pinging the switch from the VM, which also failed.
I found two things I had overlooked:
- Adding the VLAN (tagged) to the interfaces of the (temporary) switch my cluster is plugged into.
-
Adding routes to the OpenBSD box:
route add 192.168.10/24 192.168.11.250 route add 192.168.20/24 192.168.11.250
Once I corrected these, I was able to SSH to the OpenBSD host.
Bastion configuration
Making routing configuration persistent
To make the manual routes persistent, I needed to add them to /etc/hostname.<interface>
- in my VM’s case /etc/hostname.vio0
:
!route add 192.168.10/24 192.168.11.250
!route add 192.168.20/24 192.168.11.250
User configuration
I added a new user, ansible
, for automation - this user will need doas
access to allow Ansible to configure the system but my user will not need to escalate to jump through the host:
doas adduser
# [Add ansible user with password manually stored in vault]
# Add ansible user to doas:
echo "permit ansible as root" >> /etc/doas.conf
# Remove my existing user from wheel
usermod -S '' my_user
Installing Python
The next thing I wanted to do was to install Python, which proved harder than I thought. Initially, but Googling for “install python on openbsd”(other search engines are available) I found a Reddit post comment which suggested installing the package pkglocatedb
to use pkg_locate
to search for a binary.
Setting up repository access
The first hurdle was that my Basion could not access the internet (by design), so I needed to add access to the OpenBSD CDN via my mirror proxy - this was just a case of adding it to the list in the ansible variables:
mirror_proxies:
#...
- name: openbsd
upstream: https://cdn.openbsd.org/pub/OpenBSD/
description: OpenBSD main repositories
and re-running my site.yaml
playbook against the mirror.
I then needed to update the /etc/installurl
on the OpenBSD box to use the proxy:
http://mirror/openbsd
Getting the right version of Python
pkg_locate bin/python3
showed me multiple different versions, and being unsure which was the “correct” or “default” version I looked online for more information. I found the OpenBSD FAQ on Package Management which suggested adding --
to install default version, which I also tried (pkg_add python--
) but was still prompted for which version to install from the possible matches. After much more Googling, I found another Reddit comment which told me to use pkg_add -I python3
. Which worked.
Ansibleising the configuration
I turned the above steps into a bootstrap-openbsd.yaml
playbook (with the hope of one day merging it with my existing, Linux, playbook), mostly using ansible.builtin.raw
module and some carefully crafted commands to make it idempotent, as Python will not be available to use other modules yet (although the routes are a notable exception to the idempotence in terms of being “changed” to the same value even if already correct):
---
# Temporary file to configure OpenBSD bastion, until merged
# with main bootstrap.yaml and/or site.yaml config.
# (existing host lookup and domain grouping config goes here - XXX needs to be separated to included playbook)
- name: Network configuration
# XXX specific to this bastion
hosts: k7-mgmt
gather_facts: false
tasks:
# Not detectably (for `changed_when`) idempotent (substitution happens even if already correct)...
- name: Routes exist
become: true
ansible.builtin.raw: "grep -q ^{{ match | ansible.builtin.quote }} /etc/hostname.{{ interface | ansible.builtin.quote }} && sed -i s_^{{ match | ansible.builtin.quote }}.\\*\\$_{{ value | ansible.builtin.quote }}_ /etc/hostname.{{ interface | ansible.builtin.quote }} || echo {{ value | ansible.builtin.quote }} >> /etc/hostname.{{ interface | ansible.builtin.quote }}"
vars:
target: '{{ item }}'
via: 192.168.11.250 # Switch
interface: vio0
match: '!route add {{ target }}'
value: '!route add {{ target }} {{ via }}'
loop:
- 192.168.10/24 # Management network
- 192.168.20/24 # Desktop/main network
- name: Installation source is configured
hosts: openbsd
gather_facts: false
tasks:
- name: Package source is correct
become: true
ansible.builtin.raw: '[ "$( cat {{ url_file | ansible.builtin.quote }} )" == {{ url | ansible.builtin.quote }} ] && echo correct || echo {{ url | ansible.builtin.quote }} > {{ url_file | ansible.builtin.quote }}'
register: installurl_output
changed_when: installurl_output.stdout | trim != "correct"
vars:
url_file: /etc/installurl
url: "{{ local_mirror.openbsd.uri | default('https://cdn.openbsd.org/pub/OpenBSD') }}"
- name: Python is installed
hosts: openbsd
gather_facts: false
tasks:
- name: Python is installed
become: true
ansible.builtin.raw: "which python3 && echo already-installed || pkg_add -I python3"
register: pkg_add_output
changed_when: pkg_add_output | trim != 'already-installed'
- name: Useful core utils are installed
hosts: openbsd
tasks:
- name: Packages are installed
become: true
ansible.builtin.package:
name: pkglocatedb
...
I added the host to my inventory.yaml
file:
internal:
hosts:
#...
k7-mgmt:
proxmox_vms:
hosts:
k7-mgmt:
proxmox_vm:
vlan: 11
openbsd:
hosts:
k7-mgmt:
# When all bastions are openbsd
#children:
# bastions:
vars:
ansible_become_method: doas
and the local mirror to group_vars/domain_home_entek_org_uk.yaml
:
local_mirror:
#...
openbsd:
uri: http://mirror/openbsd
Because Python wasn’t installed, I had to change the domain to be hardcoded for internal
group hosts in group_vars/internal.yaml
rather than rely on a fact (which I left set to '{{ ansible_facts.domain }}'
in group_vars/all.yaml
):
domain: home.entek.org.uk
Using the Bastion
ProxyJump
ssh option:
Host k7-mgmt
HostName 192.168.11.100
User laurence
Host pve01
User ansible
HostName 192.168.10.51
ProxyJump k7-mgmt
To be continued…
There is still some work to do to secure the new Bastion and enforce that access to the management VLAN only goes via it, but this will be the subject of a subsequent post.