How to Build an Ubuntu Bastion Host for VCFA 9.x and Holodeck

I started using HoloDeck in earnest late last year. The ease of deploying and redeploying VCF 9 environments has been a godsend as I learn some of the new VCF Automation features.

The Holorouter and Webtop were good enough to get started. They were close to the environment, already had useful tools, and saved me from building another machine before I understood what I was trying to reach.

I quickly discovered that neither of the available working environments was particularly well suited to development. I couldn’t install the applications I needed in the Webtop terminal, which made it difficult to work with Kubernetes, perform meaningful development, or even track and save my work with Git.

Using the router OS prompt came with its own risks. For example, don’t even think about installing VCF.PowerCLI—it can break the PowerShell scripts used to deploy HoloDeck.

Then there was the time-consuming process of copying information out of the environment. Whether it was a template, a certificate, or a piece of command output, getting it back to my Mac required a multistep workflow: save it to a text file, SSH into another system, transfer it with SCP, and then retrieve it locally.

The process worked, but it felt like using a filing cabinet to pass a sticky note across the room. To put it mildly, it made about as much sense as putting a screen door on a submarine.

Webtop got us moving, but using it for everyday file transfer felt a little like installing a screen door on a submarine.

This article keeps things practical. It explains why I built the bastion, how its two network connections work, which routes it uses, and how to connect through it.

The answer was a small Ubuntu bastion host: a normal SSH destination, a place to run the VCF CLI and kubectl, and an SSH SOCKS5 bridge for browser and API access from my Mac.

The addresses below are sanitized examples from a VCFA 9.1 HoloDeck environment. Replace them with the values from your own environment before running commands.

The Mac stays comfortable on the outside while the bastion handles the route into HoloDeck.

What the bastion is for

The bastion gives me one reliable place to work from:

Mac
|
| normal SSH, file transfer, and optional SOCKS5 tunnel
v
Ubuntu bastion
|
| ens34 on VM Network, static management address
|
| ens37 on Holo-PG-A, static transit routes
v
Holorouter -> HoloDeck environment

From my Mac, the bastion behaves like an ordinary Linux host. That fixes the copy-and-paste problem immediately. It also gives me a clean home for tools, scripts, temporary certificates, and command history without making the Holorouter or Webtop carry the entire workflow.

The bastion is not a replacement for the Holorouter. It is a carefully placed client of the HoloDeck routing path.

The two network connections

The primary interface connects to the vSphere port group named:

VM Network

For this article, use a static example address in a separate private management network:

192.168.50.10/24

The address is illustrative only. Replace it with an unused address, prefix, gateway, and DNS settings that belong to your own VM Network. Do not use 192.168.50.10/24 blindly, and do not choose a range that overlaps the HoloDeck routes or your local Mac network.

Use this side for ordinary SSH management from the Mac. A static address makes the bastion easier to find after a reboot, but it also makes address management your responsibility. Record the assignment in your normal IPAM or lab notes.

The second interface connects to:

Holo-PG-A

Holo-PG-A is the native VLAN for the site-a HoloDeck environment. This is the internal, Holorouter-facing connection. On the bastion, it appears as ens37 and connects to the following transit network:

10.1.10.128/25

The validated example used:

ens37 address: 10.1.10.229/25
Holorouter next hop: 10.1.10.129

The exact address can change. The interface role and next hop are the important parts.

One interface preserves normal management; the other owns the specific routes into HoloDeck.

Create the Ubuntu VM

Create a small Ubuntu VM using the normal VCFA or vSphere process for your environment. The bastion does not need to be large for this job; it needs reliable networking and enough disk for tools, logs, and temporary artifacts.

Attach two virtual network adapters:

  1. The first adapter to VM Network for static management access.
  2. The second adapter to Holo-PG-A for HoloDeck transit access.

Install Ubuntu and enable SSH. During the first login, update the system and install the small set of tools used throughout this article:

sudo apt update
sudo apt upgrade
sudo apt install -y \
curl \
dnsutils \
git \
jq \
netcat-openbsd \
network-manager \
openssh-server
  • curl tests HTTP and HTTPS endpoints.
  • dnsutils provides DNS troubleshooting commands such as dig and nslookup. The article uses getent, which Ubuntu provides separately through the base system.
  • git provides version control for scripts and configuration files.
  • jq filters and formats JSON returned by APIs and command-line tools.
  • netcat-openbsd performs simple TCP connectivity tests when an application-level request is not appropriate.
  • network-manager provides nmcli, which is used to configure the two interfaces and persistent routes.
  • openssh-server provides remote shell access, file transfers, and the SOCKS5 tunnel.

Only curl, network-manager, and openssh-server are essential to the connectivity workflow demonstrated here. Git supports the bastion’s development role, while dnsutils, jq, and netcat-openbsd are useful troubleshooting tools. Tools such as the VCF CLI and kubectl can be installed separately when their workflows are introduced.

I opted to use SSH key authentication primarily so I could give a Codex agent secure access to the environment for testing and developing scripts and solutions. That experience is a blog article in its own right—and one I’m looking forward to writing.

Identify the interfaces before adding routes

Ubuntu interface names depend on the image and installation. Do not assume that the management adapter is always ens34 or that the transit adapter is always ens37.

On the bastion, inspect the addresses and connection names:

ip -br address
nmcli device status
nmcli connection show

For the rest of this article, the example assumes:

  • ens34 is connected to VM Network and uses the static management address.
  • ens37 is connected to Holo-PG-A and uses the static transit address.

If your interface names differ, substitute them consistently.

Configure the management interface

Set the management address according to your local gateway and DNS design. The following is an illustrative NetworkManager example only:

MGMT_CONNECTION="$(
nmcli -g GENERAL.CONNECTION device show ens34
)"
sudo nmcli connection modify \
"${MGMT_CONNECTION}" \
ipv4.method manual \
ipv4.addresses '192.168.50.10/24' \
ipv4.gateway '192.168.50.1' \
ipv4.dns '192.168.50.1'

Replace 192.168.50.1 with the real management gateway and DNS server, or omit the DNS value if your environment supplies name resolution another way. Confirm the management route before applying changes over SSH.

Configure the HoloDeck-facing interface

First identify the NetworkManager connection attached to ens37:

ENS37_CONNECTION="$(
nmcli -g GENERAL.CONNECTION device show ens37
)"
printf 'ens37 connection: %s\n' "${ENS37_CONNECTION}"

Set the transit address and prevent this interface from installing a second default route:

sudo nmcli connection modify \
"${ENS37_CONNECTION}" \
ipv4.method auto \
ipv4.never-default yes

The ipv4.never-default yes setting matters. The bastion should keep its normal management default route on the VM Network interface. The HoloDeck-facing interface needs specific routes, not a competing default route.

Add all of the HoloDeck routes

The route table that matters is the one installed through ens37:

10.1.0.0/16 via 10.1.10.129 dev ens37 proto static metric 101
10.1.10.128/25 dev ens37 proto kernel scope link src 10.1.10.229 metric 101
10.2.0.0/16 via 10.1.10.129 dev ens37 proto static metric 101
10.100.0.0/27 via 10.1.10.129 dev ens37 proto static metric 101

The following output shows the Holorouter interface connected to the same transit network. Its address, 10.1.10.129, becomes the next hop for the bastion’s static routes:

3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8000 qdisc mq state UP group default qlen 1000
link/ether 00:0c:29:bc:87:9f brd ff:ff:ff:ff:ff:ff
altname eno2
altname enp19s0
altname ens224
inet 10.1.10.129/25 scope global eth1
valid_lft forever preferred_lft forever
4: eth2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000

The connected route is created by the interface address. The three static routes reach the wider HoloDeck networks through the Holorouter at 10.1.10.129:

  • 10.1.0.0/16: HoloDeck site-a services and transit networks.
  • 10.2.0.0/16: future or current VCFA workload networks.
  • 10.100.0.0/27: Supervisor Kubernetes management-plane services in the validated lab.

Preserve any routes already present on the connection. The following example expresses the intended static route set:

The route table captures both validated service paths and future workload reachability.
sudo nmcli connection modify \
"${ENS37_CONNECTION}" \
ipv4.routes \
"10.1.0.0/16 10.1.10.129 101,10.2.0.0/16 10.1.10.129 101,10.100.0.0/27 10.1.10.129 101"
sudo nmcli device reapply ens37

If NetworkManager reports that a reapply is not possible, schedule a controlled connection restart or reboot. Do not casually cycle the only interface you are using for SSH access.

Check route ownership

The route table should make the intended ownership visible:

ip route
ip route get 10.100.0.2
ip route get 10.2.0.10

The Supervisor example should report a path similar to:

via 10.1.10.129 dev ens37 src 10.1.10.229

The last address is only an example from the 10.2.0.0/16 workload range. Do not treat it as proof that a workload exists. It proves that the route is selected; a real connection is needed to prove the destination path.

Also confirm that the management default route still belongs to the VM Network side:

ip route show default

Validate DNS and vCenter reachability

Test the path from the bastion before involving the Mac browser:

getent ahostsv4 vc-mgmt-a.site-a.vcf.lab
curl -skI \
--connect-timeout 5 \
--max-time 15 \
https://vc-mgmt-a.site-a.vcf.lab/

In the validated environment, vCenter returned an HTTPS response through the bastion, confirming that DNS resolution and network connectivity were working. For a trusted final configuration, install the appropriate vCenter CA certificate and repeat the request without -k. The insecure option is useful for isolating routing and certificate issues in a lab, but it should not become part of the permanent configuration.

You can also test connectivity to the Supervisor Kubernetes API server endpoint:

curl -sk \
--connect-timeout 5 \
--max-time 15 \
https://10.100.0.2/version

An authentication response can still prove that the network and TLS endpoint were reached. A timeout points to routing, firewall, or service availability rather than a bad Kubernetes command.

Use SSH normally from the Mac

Connect to the bastion through its static address on VM Network:

ssh ubuntu@<bastion-management-ip>

For repeat use, add a host entry to the Mac’s SSH configuration:

Host holodeck-bastion
HostName <bastion-management-ip>
User ubuntu
IdentityFile ~/.ssh/<bastion-key>

Now copying commands, editing files, and using kubectl feel like normal work again. Files can move directly with scp when needed:

scp ./local-file.txt holodeck-bastion:/tmp/local-file.txt
scp holodeck-bastion:/tmp/result.txt ./result.txt

Add a SOCKS5 tunnel when the browser needs the route

SSH can also create a local SOCKS5 listener on the Mac:

ssh -N \
-D 127.0.0.1:1080 \
holodeck-bastion

The listener is bound to localhost, so it is not exposed to the rest of the local network.

Configure Firefox or another browser with:

SOCKS host: 127.0.0.1
Port: 1080
SOCKS v5: enabled
Proxy DNS: enabled

Proxy-side DNS is important for names such as vc-mgmt-a.site-a.vcf.lab. Without it, the Mac may try to resolve the internal name locally before the request ever reaches the bastion.

Validated Firefox SOCKS Proxy Settings.

Test the tunnel from the Mac with socks5h, where the h means hostname resolution happens through the proxy:

curl -sk \
--proxy socks5h://127.0.0.1:1080 \
--connect-timeout 5 \
--max-time 15 \
https://vc-mgmt-a.site-a.vcf.lab/

This is the part Webtop never made especially pleasant. The browser stays on the Mac, copy and paste stays on the Mac, and the bastion supplies the route into the HoloDeck environment.

Here is a screenshot of the Firefox tabs on my Mac. As you can see, the SOCKS5 tunnel provides access to several deployed HoloDeck resources.

Remote Firefox access through a SOCKS proxy into the HoloDeck environment

Why this route is worth keeping

The bastion is deliberately uncomplicated:

  • Management arrives through VM Network using a static example address.
  • HoloDeck traffic leaves through Holo-PG-A and ens37.
  • The normal default route remains on the management side.
  • Specific HoloDeck networks use the Holorouter next hop.
  • SSH provides the working session and file transfer.
  • SOCKS5 provides browser and API access when the Mac cannot route directly.

This avoids changing the Holorouter’s VRF behavior, BGP, reverse-path filtering, proxy configuration, or system-wide socket behavior. Those components can remain responsible for the routing they already own.

What the bastion proves, and what it does not

A successful vCenter connectivity test proves that the bastion can resolve the vCenter hostname, route traffic to the destination, and receive an HTTPS response. It does not prove that every HoloDeck service or workload is reachable. Each destination still requires its own connectivity and application-level validation.

Similarly, the persistent 10.2.0.0/16 route proves that the bastion knows where future VCFA workload traffic should go. It does not prove SSH to a real workload until one exists and a connection has been tested.

Once workload access is configured, the same path can provide HTTP or SSH access to deployed machines. For example, the following screenshot shows access to a load-balanced vSphere Pod deployed through VCF Automation. That workflow will be the subject of a future article.

Keeping those evidence boundaries separate is the difference between “the route is configured” and “the application path works.”

Next steps

With the bastion in place, I now have a practical workstation for managing and developing against the HoloDeck environment. It provides a central location for running the VCF CLI, kubectl, diagnostic utilities, and other development tools without modifying the router OS or relying on Webtop’s cumbersome file-transfer process. It also gives me a clean foundation for testing additional services and workflows in future articles.

Disclaimer: Some of the stuff you see here has been checked out, tweaked, or even created by AI. Welcome to the new age, folks!

Basic vRA Endpoint workflow progress

Some really good progress has been made this week, to include;

  • Adding NvIPAM as legitimate vRA Endpoint
  • Get IP Rangescropped-nvipam-1.png
  • Allocate IP from IPAM
  • Release IP to IPAM

The first, took off after looking at the code in the SDK package.  One of the main things I found was it required two actions, and four workflows.  I simply copied the ones listed int the SDK into my own folders, and off I went.  Actually the only rea

EndpointType

l change was just changing the Id’s in my copied workflow to match the action path, and the workflow ID’s.  Danged if it didn’t get added the first time.

vRAEndpoint

 

After the type was added, I simply went in added my IPAM server as  an NvIPAM IPAM endpoint.

Get IP Ranges workflow took some major rework as the SDK version uses hard-coded pools, and did not have support for a token authentication.  Bearer tokens will be used throughout the project so an action was developed for reuse.  The username, password and baseUrl are provided by vRA as an Endpoint composite type.

After making some additional changes to one of the actions and the workflow, I was able to add an IPAM pool to vRA, and assign it to a reservation. The Range Name is generated by IPAM by appending the pool to the network to simplify pool discovery (See previous posts).

NvIPAMNetworkProfile

The basic allocate and release workflows are also working for basic External network IP management.  I’ll go into more detail about those in later posts.

DNS management is next on the list.  Stay tuned.

[twitter-follow screen_name=’knotacoder’]

 

 

NvIPAM Ansible Playbook

Man I love it when a play starts to come together. cropped-nvipam-1.png

First off, the refactoring is done.  Oh yeah.

Plus an Ansible playbook to prepare and setup NvIPAM on a basic CentOS virtual machine is now working.  The playbook installs and configures the following

  1. Installs the basic OS requirements
  2. Installs and configures Postgresql
  3. Installs and configures PowerDNS authoritative and recursor servers
  4. Installs and configures Python virutal environment
  5. Installs and configures NGINX
  6. Setups uWSGI
  7. And installs the application

And if that isn’t enough, I dumped the Swagger into a Postman file to help with continuing development agains vRealize Orchestrator (and other CMS).

The current install script is available at NvIPAM setup

The next step is to start tinkering with the vRA IPAM SDK.

[twitter-follow screen_name=’knotacoder’]