<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-01-27T18:43:21-05:00</updated><id>/feed.xml</id><title type="html">Clark Hale’s Blog</title><subtitle>This is the personal site of Clark Hale.  All opinions expressed here  are his own.</subtitle><author><name>Clark Hale</name></author><entry><title type="html">RHEL on Raspberry Pi 4 and provisioning with Satellite</title><link href="/blog/linuxunix/2026/01/27/rhel9-raspberry-pi-with-satellite.html" rel="alternate" type="text/html" title="RHEL on Raspberry Pi 4 and provisioning with Satellite" /><published>2026-01-27T18:30:00-05:00</published><updated>2026-01-27T18:30:00-05:00</updated><id>/blog/linuxunix/2026/01/27/rhel9-raspberry-pi-with-satellite</id><content type="html" xml:base="/blog/linuxunix/2026/01/27/rhel9-raspberry-pi-with-satellite.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>I need a few more machines in my lab (who doesn’t?), and normally I’d
just create more virtual machines on one of my big servers.  But those
servers are at capacity, and the prices on new servers (and especially
RAM) is really high.</p>

<p>Also, those large servers tend to use a lot of energy and have heavy
cooling requirements.  This means lots of heat and noisy fans.  I want
to minimize the heat and noise I add.  The room I have dedicated to
servers is generally 5-10 degrees farenheit warmer than the rest of my
house and I can hear the fan noise from adjacent rooms.</p>

<p>I’m wondering if, instead of a big server, having a fleet of very
inexpensive small machines could work better?</p>

<p>I’ve experimented with running silent, low heat systems long ago when
VIAs C3 and Intel Atom in Micro-ITX formats were en vogue.  At the
time, those systems disappointed me in terms of performance.  Now,
almost 2 decades later, I figure it’s worth another try.</p>

<p>In 2026, it seems like the way forward for inexpensive low power,
low heat, low noise systems might be ARM processors.  There’s a lot of
variety out there, but Raspberry Pis seem to be the an immediate
starting point since they have a well established ecosystem and a lot
of community support.</p>

<p>At between USD$70-90 (Feb 2026 prices) for a Raspberry Pi 4 w/ 8GB of
RAM, it may work out financially to be cheaper than buying a large
server, but it also has to work and fulfill my requirements!</p>

<h1 id="companion-videos">Companion Videos:</h1>

<p>To go with these blog post, I’ve created some videos.  A time of
writing the video series is still in development, but will be updated
here as they are released.</p>

<ul>
  <li><a href="https://youtu.be/fMMosZam0qU?si=xpVO4HSeiPyMZ_2f">Racking Raspberries Episode 1: Testing Out Uctronics Pi Rack Pro</a></li>
</ul>

<h1 id="personal-requirements">Personal Requirements</h1>

<p>I have several end-requirements:</p>

<ol>
  <li>Must be able to mount in a standard 19” rack.</li>
  <li>Must be able to remotely manage the power state</li>
  <li>Must be able to remotely manage the console</li>
  <li>Must run Red Hat Enterprise Linux (and ideally CoreOS, too)</li>
  <li>Must be able to provision over the network and ideally through Red
Hat Satellite</li>
</ol>

<p>The last two requirements are probably going to be the hardest to
fulfill.  Raspberry Pis are not actually supported by Red Hat, but
there do appear to be work-a-rounds posted on the internet.</p>

<h1 id="hardware">Hardware</h1>

<p>For the first requirement, there are several Raspberry Pi rack
mounting solutions available: from simple brackets to full, modular
enclosures.  I decided on purchasing a more complicated solution that
could support Power-over-Ethernet (PoE) hats and 2.5” SATA drives: the
<a href="https://www.uctronics.com/raspberry-pi/1u-rack-mount/uctronics-pi-rack-pro-for-raspberry-pi-4b-19-1u-rack-mount-support-for-4-2-5-ssds.html">UCTonics Pi Rack
Pro</a>.</p>

<p>This chassis allows for 4 Raspberry Pis per rack unit.  Some of the
more basic brackets allow for much higher density, but at the expense
of supporting add-ons like the PoE hat.  I think 4 Pis per Rack Unit
is a good compromise for me.</p>

<p>The chassis is going for a “blade module” vibe, but it really falls
short on that.  Really, it’s just 4 simplistic trays with some add-on
boards.  The build quality is OK, but there are too many screws and
there are some cable routing issues, notably around the HDMI cable.  I
wish that, like the SD Card, they had contrived some way to bring the
HDMI port to the front of the unit.  The LED panel is a neat addition,
but the power button is kind of useless.  Unfortunately, the LED
screen and power button add-ons are not well supported in any Linux
distributions and drivers are only as source code.</p>

<p>That being said, I’m reasonably happy with this enclosure, but I’m not
sure the almost $300 price point is justified.</p>

<p>Still, this checks off requirement #1:  Must be able to mount in a standard 19” rack.</p>

<p>Using standard Raspberry Pi <a href="https://www.raspberrypi.com/products/poe-hat/">PoE
Hats</a>, I’m able to
power these machines via PoE.  In addition, with the right switching
hardware, I can remotely control the power to each port to provide
remote power management.</p>

<p>This checks off requirement #2: Must be able to remotely manage the
power state.</p>

<p>For being able to remotely view the console, there are various options
including projects like PiKVM and products like NanoKVM.  These make
what was once a pricey endeavor, adding out-of-bounds-management to a
computer, relatively inexpensive.</p>

<p>Despite that, I still don’t want to add a PiKVM or NanoKVM per
Raspberry Pi.  While these solutions are cheap individually, the cost
still adds up when you have to buy them in bulk!</p>

<p>So, I decided to buy a <a href="https://www.tesmart.com/products/hks1601-l23?srsltid=AfmBOor6iBQvMmHMv5_bxD1Hp3n8IS3AQktZ6pjFFfFYu90uZsKnqmLg">TESmart 16-port Rack Mount
KVM</a>.
This does not allow remote viewing over network, it is simply
switching 16 different KVM inputs to a single output.  While it
doesn’t capture video output, it can be controlled remotely, either
via network or RS-232.  The thought here is to connect a SINGLE PiKVM,
NanoKVM or similar to the KVM switch.</p>

<p>I can then hook up to 16 Raspberry Pis (4 UCTronics Chassis) to the
KVM Switch and through a combination of the remote control on the KVM
and PiKVM or NanoKVM, I can interact with all 16 Raspberry Pis
consoles over the network, albeit not at the same time.  I think this
is a reasonable compromise.</p>

<p>Cost-wise, I think this makes sense.  A NanoKVM is roughly USD$55, so
sixteen of them would cost USD$880!  That’s not including costs for
cables and extra ports on an switch.  By contrast, this KVM switch
cost me about USD$300 and I’ve seen similar models for anywhere
between USD$250-600.</p>

<p>For this to be truly seamless, I’ll likely have to modify the
software of the PiKVM or NanoKVM also be able to control the KVM
Switch.  I’m not ready to take on that project yet, but there is a
clear path forward.  So, I consider this checking off my requirement
#3 “Must be able to remotely manage the console”.</p>

<p>With the UCTronics chassis, PoE Hats, TESmart KVM Switch, and remote
KVM solution (like PiKVM or NanoKVM), I think I have all my hardware
requirements satisfied, and I can move on to the RHEL and Satellite.</p>

<h1 id="red-hat-enterprise-linux">Red Hat Enterprise Linux</h1>

<p>Since I’m planning use these Raspberry Pis to replace x86_64 servers,
I need to be able to run RHEL or RHEL adjacent Operating Systems like
CentOS or Fedora.</p>

<p>This is a hurdle, because <em>Raspberry Pis are not a supported hardware
platform for RHEL!</em></p>

<p>That being said, it <em>is</em> possible to run RHEL on Raspberry Pi, but it
requires some special handling.  Most notably, RHEL for ARM requires
that the hardware have UEFI firmware.</p>

<h2 id="uefi-background">UEFI Background</h2>

<p>UEFI originated with Intel Itanium (ia64).  The processor architecture
required new firmware and Intel/HP decided to create EFI (Extensible
Firmware Interface) rather than leverage the Open Firmware standard
used in many other workstation/server platforms.  EFI was later ported
to x86 and x86_64 as a replacement for the legacy IBM-compatible BIOS
firmware and has become the new standard for that architecture in the
past few years.  Along the way EFI got renamed UEFI (Unified
Extensible Firmware Interface) and got ported to ARM, but the uptake
in the ARM ecosystem has been spotty at best.  RHEL on ARM requires
UEFI and all supported ARM machines include UEFI in non-volatile
storage (ROM, Flash, et cetera).</p>

<p>Raspberry Pis do not include UEFI and have their own unique firmware.
However, the community has ported an open-source UEFI implementations
to the Raspberry Pi 4 and 5.  The executable binaries can be stored on
an SD card and then chain-loaded by the Raspberry Pi’s native
firmware.  Since the UEFI binaries are stored on volatile storage
(i.e. an SD Card) rather than in a non-volatile ROM or Flash memory,
I’m going to have to be careful when partitioning and installing an
Operating System.  A wrong move and I could overwrite or delete the
partition holding the UEFI binaries, thus preventing the system from
booting.</p>

<p>There are two current repositories for different versions of the Raspberry Pi:</p>
<ul>
  <li><a href="https://github.com/pftf/RPi4">UEFI Images for Raspberry Pi 4</a></li>
  <li><a href="https://github.com/NumberOneGit/rpi5-uefi">UEFI Images for Raspberry Pi 5</a></li>
</ul>

<p>NB at time of writing, Google does not bring the RPi 5 repo up in
search…instead it links to <a href="https://github.com/worproject/rpi5-uefi">an abandoned
effort</a>.  The above link
comes from a recent <a href="https://wiki.freebsd.org/arm/Raspberry%20Pi%205">FreeBSD
Wiki</a> page.  I’ve not
tested the RPi 5 at all, YMMV.</p>

<h2 id="installing-the-firmware">Installing the firmware</h2>

<p>Getting the UEFI firmware installed is a fairly straightforward
exercise.  I’m going to be using the SD card to hold the UEFI firmware
and boot from it.</p>

<p>First, wipe any existing partition table and create a new GPT
partition table on the SD card.  GPT or Global Partition Table is a
newer format that replaces the older DOS/MBR-style partition tables. A
DOS/MBR-style partition table <em>will work</em> but seems to cause issues
with the RHEL-installer.</p>

<p>Then, create a smallish partition of type EFI System.  100 MB should
be sufficient.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## My device is mmcblk0, yours may be different.  Proceed with caution or you may have data loss!

# # Clear any existing partition table:
# wipefs -a /dev/mmcblk0
/dev/mmcblk0: 8 bytes were erased at offset 0x00000200 (gpt): 45 46 49 20 50 41 52 54
/dev/mmcblk0: 8 bytes were erased at offset 0x76e47fe00 (gpt): 45 46 49 20 50 41 52 54
/dev/mmcblk0: 2 bytes were erased at offset 0x000001fe (PMBR): 55 aa
/dev/mmcblk0: calling ioctl to re-read partition table: Success

# fdisk /dev/mmcblk0 

Welcome to fdisk (util-linux 2.41.3).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table.
Created a new DOS (MBR) disklabel with disk identifier 0x4f27bc74.

###
### fdisk still defaults to DOS/MBR-style partition tables
### the 'g' command will force it into GPT mode.
###

Command (m for help): g
Created a new GPT disklabel (GUID: 714CC8E9-2EE4-4E1D-80B2-F088DE7F3D35).

Command (m for help): n
Partition number (1-128, default 1): 1
First sector (2048-62333918, default 2048): 2048
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-62333918, default 62332927): +100M

###
###  There may still be existing data and filesystem headers on partitions
###  they'll be erased later, but it's no harm to remove it now
###

Created a new partition 1 of type 'Linux filesystem' and of size 100 MiB.
Partition #1 contains a vfat signature.

Do you want to remove the signature? [Y]es/[N]o: y

The signature will be removed by a write command.

###
### Setting the partition type to EFI System (Type 1) is
### absolutely required.  Otherwise things won't boot!
###
Command (m for help): t
Selected partition 1
Partition type or alias (type L to list all): 01
Changed type of partition 'Linux filesystem' to 'EFI System'.

Command (m for help): p
Disk /dev/mmcblk0: 29.72 GiB, 31914983424 bytes, 62333952 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 714CC8E9-2EE4-4E1D-80B2-F088DE7F3D35

Device         Start    End Sectors  Size Type
/dev/mmcblk0p1  2048 206847  204800  100M EFI System

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
</code></pre></div></div>

<p>Next, make a VFAT filesystem on that partition and extract the EFI
archive at the root of that filesystem.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># mkfs.vfat /dev/mmcblk0p1 
mkfs.fat 4.2 (2021-01-31)
# mount /dev/mmcblk0p1 /mnt
# pushd /mnt
# unzip /path/to/RPi4_UEFI_Firmware_v1.50.zip 
Archive:  /path/to/RPi4_UEFI_Firmware_v1.50.zip
  inflating: RPI_EFI.fd              
  inflating: bcm2711-rpi-4-b.dtb     
  inflating: bcm2711-rpi-400.dtb     
  inflating: bcm2711-rpi-cm4.dtb     
  inflating: config.txt              
  inflating: fixup4.dat              
  inflating: start4.elf              
   creating: overlays/
  inflating: overlays/miniuart-bt.dtbo  
  inflating: overlays/upstream-pi4.dtbo  
  inflating: Readme.md               
   creating: firmware/
  inflating: firmware/LICENCE.txt    
   creating: firmware/brcm/
  inflating: firmware/brcm/brcmfmac43455-sdio.clm_blob  
  inflating: firmware/brcm/brcmfmac43455-sdio.bin  
  inflating: firmware/brcm/brcmfmac43455-sdio.txt  
  inflating: firmware/brcm/brcmfmac43455-sdio.Raspberry  
  inflating: firmware/Readme.txt     
# popd 
# umount /mnt
# sync
</code></pre></div></div>

<p>Now the SD card should be ready.  The Pi should boot into the UEFI firmware:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/first_uefi_boot.png" alt="RPI UEFI Boot Screen" />
  <figcaption>RPI UEFI Boot Screen</figcaption>
</figure>
</div>

<h2 id="creating-rhel-installation-media">Creating RHEL Installation Media</h2>

<p>RHEL can be downloaded from <a href="https://access.redhat.com">Red Hat’s
portal</a>.  Since I work for Red Hat, I have
an employee subscription, but <a href="https://developers.redhat.com/articles/faqs-no-cost-red-hat-enterprise-linux">no cost RHEL
subscriptions</a>
are available for selected use cases.</p>

<p>I always use the Boot ISO image.  This ISO image doesn’t have packages
and will need network connectivity to either Satellite, a repository
server, or the Red Hat CDN to complete an install.  The Full ISO
images are now exceeding 10 GiB, and no longer fits on my USB flash
drives.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/rhel_download.png" alt="RHEL 9.7 Download" />
  <figcaption>RHEL 9.7 Download</figcaption>
</figure>
</div>

<p>In Fedora, the Fedora Media Writer can be used to write an ISO image
to a USB drive.</p>

<p>There are so many tools out there, so use whatever is
available/convenient.  As a last resort, a simple <code class="language-plaintext highlighter-rouge">dd</code> command can be
used:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># dd if=/path/to/rhel.iso of=/dev/myusbdrive
</code></pre></div></div>

<p>Just be sure to ensure the iso is the ARM64 (aarch64) version!  I’ve
accidentally wasted several hours of my life by not paying close
attention and trying to use the x86_64 version.</p>

<h2 id="pre-requisite--enable-more-than-3-gib-of-ram">Pre-requisite:  Enable more than 3 GiB of RAM</h2>

<p>In the RPi4 UEFI firmware, there is a default setting that restricts
the usable amount of RAM to 3 GiB.  This was to work around a bug
present in some versions of the Linux kernel.  This default seems,
quite frankly, out of date since affected Linux kernel versions are
several years old.</p>

<p>RHEL 9 has a relatively recent kernel, and does not suffer from this
bug.  So, we should disable this limitation and use the full amount of
RAM.</p>

<p>I accidentally forgot to change this setting, and noticed a high
incident of failures during installation because of ram disk
corruption.  Once I enabled the full amount of RAM, this issue
disappeared completely.</p>

<p>To change this setting and enable the full amount of RAM:</p>

<ol>
  <li>On boot, press <code class="language-plaintext highlighter-rouge">Esc</code> to enter the UEFI settings menu:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup1.png" alt="Press Esc to enter the UEFI Settings Menu" />
  <figcaption>Press Esc to enter the UEFI Settings Menu</figcaption>
</figure>
</div>

<ol>
  <li>Select <code class="language-plaintext highlighter-rouge">Device Manager</code>:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup2.png" alt="Select Device Manager" />
  <figcaption>Select Device Manager</figcaption>
</figure>
</div>

<ol>
  <li>Select <code class="language-plaintext highlighter-rouge">Rapsberry Pi Configuration</code>:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup3.png" alt="Select Raspberry Pi Configuration" />
  <figcaption>Select Raspberry Pi Configuration</figcaption>
</figure>
</div>

<ol>
  <li>Select <code class="language-plaintext highlighter-rouge">Advanced Configuration</code>:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup4.png" alt="Select Advanced Configuration" />
  <figcaption>Select Advanced Configuration</figcaption>
</figure>
</div>

<ol>
  <li>Change <code class="language-plaintext highlighter-rouge">Limit RAM to 3 GB</code> to <code class="language-plaintext highlighter-rouge">Disabled</code>:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup5.png" alt="Change Limit RAM to 3 GB" />
  <figcaption>Change Limit RAM to 3 GB</figcaption>
</figure>
</div>

<ol>
  <li>Press <code class="language-plaintext highlighter-rouge">F10</code> to save:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup6.png" alt="Press F10 to Save." />
  <figcaption>Press F10 to Save.</figcaption>
</figure>
</div>

<ol>
  <li>Either hard reset the machine or navigate to the main menu and select <code class="language-plaintext highlighter-rouge">Reset</code>:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup7.png" alt="Select Reset" />
  <figcaption>Select Reset</figcaption>
</figure>
</div>

<ol>
  <li>On subsequent boots, the UEFI setting menu should list 8GiB of RAM:</li>
</ol>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_ram_setup8.png" alt="8GiB of RAM!" />
  <figcaption>8GiB of RAM!</figcaption>
</figure>
</div>

<h2 id="bootinginstalling-rhel">Booting/Installing RHEL</h2>

<p>To boot from the USB thumb drive, access the EFI settings menu by pressing <code class="language-plaintext highlighter-rouge">Esc</code>.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup1.png" alt="Press Esc to enter the UEFI Settings Menu" />
  <figcaption>Press Esc to enter the UEFI Settings Menu</figcaption>
</figure>
</div>

<p>Then navigate to the <code class="language-plaintext highlighter-rouge">Boot Manager</code> menu</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_manager.png" alt="RPI UEFI Boot Manager" />
  <figcaption>RPI UEFI Boot Manager</figcaption>
</figure>
</div>

<p>And select the USB devices that has the installation media.   In my case, it’s I used a SanDisk Cruzer USB drive, so it’s relatively easy to find.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_usb_select.png" alt="RPI UEFI Boot Manager:  Selecting the USB Drive" />
  <figcaption>RPI UEFI Boot Manager:  Selecting the USB Drive</figcaption>
</figure>
</div>

<p>After this RHEL boots and the installation process is basically the same as RHEL on x86_64!</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/grub_rhel_booting.png" alt="RHEL Booting!" />
  <figcaption>RHEL Booting!</figcaption>
</figure>
</div>

<p><strong>ONE MAJOR EXCEPTION IS THAT YOU MUST PROTECT THE EFI SYSTEM PARTITION FROM BEING OVERWRITTEN!</strong></p>

<p>If the RHEL installer reformats the EFI partition, or wipes the
partition table completely, then the UEFI binaries that were installed
will, of course, go away.  This is the trouble with having the EFI
binaries on an SD card as opposed in a ROM or on-board Flash memory.</p>

<p>Manual partitioning is required guarantee the EFI System partition is
protected, but it was a bit tricky.  There seem to be some bugs in
Anaconda (the RHEL Installer) that cause it to freeze or incorrectly
think the EFI partition is on LVM.</p>

<p>In the end, this is the partition table I settled on:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/rhel_partition_table.png" alt="Partition Table" />
  <figcaption>Partition Table</figcaption>
</figure>
</div>

<p>Again, it’s very important not to reformat the EFI System Partition!</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/rhel_efi_partition.png" alt="Do not check reformat!" />
  <figcaption>Do not check reformat!</figcaption>
</figure>
</div>

<p>Once the install is successful, Anaconda will generate a kickstart
file that can be used to repeat the installation.  This kickstart file
can be used as a template for other machines, so that we can avoid
using the installer GUI in the future.</p>

<p>After installation, the generated kickstart should be located in
<code class="language-plaintext highlighter-rouge">/root/anaconda-ks.cfg</code>.</p>

<p>The kickstart is pretty standard, with the major customization being in the disk partitioning section:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Generated using Blivet version 3.6.0
ignoredisk --only-use=mmcblk1,sda
# System bootloader configuration
bootloader --append="crashkernel=1G-4G:256M,4G-64G:320M,64G-:576M" --location=mbr --boot-drive=mmcblk1
# Partition clearing information
clearpart --none --initlabel
# Disk partitioning information
part /boot/efi --fstype="efi" --noformat --onpart=mmcblk1p1 --fsoptions="umask=0077,shortname=winnt"
part pv.2270 --fstype="lvmpv" --ondisk=sda --size=4095
part /boot --fstype="xfs" --ondisk=mmcblk1 --size=1024
part / --fstype="xfs" --ondisk=mmcblk1 --size=20480
volgroup rhel_rpi1 --pesize=4096 pv.2270
logvol swap --fstype="swap" --size=4092 --name=swap --vgname=rhel_rpi1
</code></pre></div></div>

<p>This is not the exact partition scheme I wanted, but it’s close
enough, and is a good starting point for a more refined configuration.</p>

<p>Since I’m able to get a basic RHEL installation working, this checks
off my requirement #4 “Must run Red Hat Enterprise Linux”!</p>

<p>Now, hopefully, after rebooting at the end of installation, RHEL comes
up without any additional effort.  This does seems to be the case with
the latest version of the UEFI Firmware (1.50) and RHEL 9.7.  Earlier
versions of either RHEL or the UEFI firmware had issues with
configuring a boot entry for RHEL.  That configuration needed to be
manually done after installation.</p>

<h2 id="configuring-the-efi-boot-menu">Configuring the EFI boot menu</h2>

<p>This section is <em>probably</em> no longer needed, but may be useful in case
the installer doesn’t create a boot entry, or if there is a desire to
create a custom one for some reason.</p>

<p>First, enter the EFI settings menu by pressing <code class="language-plaintext highlighter-rouge">Esc</code> during the boot sequence:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup1.png" alt="Press Esc to enter the UEFI Settings Menu" />
  <figcaption>Press Esc to enter the UEFI Settings Menu</figcaption>
</figure>
</div>

<p>Next, navigate to <code class="language-plaintext highlighter-rouge">Boot Maintenance Manager</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup2.png" alt="Enter Boot Maintenance Manager" />
  <figcaption>Enter Boot Maintenance Manager</figcaption>
</figure>
</div>

<p>Next, navigate to <code class="language-plaintext highlighter-rouge">Boot Options</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup3.png" alt="Enter Boot Option" />
  <figcaption>Enter Boot Option</figcaption>
</figure>
</div>

<p>Next, navigate to <code class="language-plaintext highlighter-rouge">Add Boot Option</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup4.png" alt="Enter Boot Option" />
  <figcaption>Enter Boot Option</figcaption>
</figure>
</div>

<p>Select the SD Card:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup5.png" alt="Select the SD Card" />
  <figcaption>Select the SD Card</figcaption>
</figure>
</div>

<p>I still have my USB Drive plugged in at this point, so it shows up as
a volume labeled ANACONDA.  The SD Card has no volume label, and will
have <code class="language-plaintext highlighter-rouge">HD(1,GPT)</code> somewhere in the middle if my instructions were
followed.</p>

<p>At this point, we are now navigating the filesystem inside the EFI System volume.</p>

<p>Navigate to <code class="language-plaintext highlighter-rouge">&lt;EFI&gt;</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup6.png" alt="Navigate to EFI" />
  <figcaption>Navigate to EFI</figcaption>
</figure>
</div>

<p>Navigate to <code class="language-plaintext highlighter-rouge">&lt;redhat&gt;</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup7.png" alt="Navigate to redhat" />
  <figcaption>Navigate to redhat</figcaption>
</figure>
</div>

<p>Select <code class="language-plaintext highlighter-rouge">shim.efi</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup8.png" alt="Select shim.efi" />
  <figcaption>Select shim.efi</figcaption>
</figure>
</div>

<p>Add a description:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setup9.png" alt="Add a description" />
  <figcaption>Add a description</figcaption>
</figure>
</div>

<p>Select <code class="language-plaintext highlighter-rouge">Commit Changes and Exit</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_menu_setupA.png" alt="Commit Changes and Exit" />
  <figcaption>Commit Changes and Exit</figcaption>
</figure>
</div>

<p>This newly created boot option will be set to the last possible boot
option.  This is likely not desired, but is easy to change using the
<code class="language-plaintext highlighter-rouge">Change Boot Order</code> screen:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_change_boot_order1.png" alt="Select Change Boot Order" />
  <figcaption>Select Change Boot Order</figcaption>
</figure>
</div>

<p>This screen can be a bit confusing.  It shows the current boot order.
The top-most item will be attempted first, and then if that fails,
each subsequent item will be attempted.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_change_boot_order2.png" alt="Boot Order Screen." />
  <figcaption>Boot Order Screen.</figcaption>
</figure>
</div>

<p>To change press <code class="language-plaintext highlighter-rouge">Enter</code>, and then use the <code class="language-plaintext highlighter-rouge">+</code> and <code class="language-plaintext highlighter-rouge">-</code> keys to move items
up and down until the desired order is displayed, and then hit <code class="language-plaintext highlighter-rouge">Esc</code>.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_change_boot_order2.png" alt="Use +/- to change order." />
  <figcaption>Use +/- to change order.</figcaption>
</figure>
</div>

<p>Now select <code class="language-plaintext highlighter-rouge">Commit Changes and Exit</code> to make save the boot order.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_change_boot_order2.png" alt="Select Commit Changes and Exit." />
  <figcaption>Select Commit Changes and Exit.</figcaption>
</figure>
</div>

<p>Now, on reboot, provided our <code class="language-plaintext highlighter-rouge">Red Hat Enterprise Linux</code> option is the
default, the Raspberry Pi should boot directly into RHEL!</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/rhel_grub.png" alt="RHEL Grub Screen!" />
  <figcaption>RHEL Grub Screen!</figcaption>
</figure>
</div>

<p>We can always manually select it by going into the <code class="language-plaintext highlighter-rouge">Boot Manager</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_mgrA.png" alt="Enter Boot Manager" />
  <figcaption>Enter Boot Manager</figcaption>
</figure>
</div>

<p>and selecting <code class="language-plaintext highlighter-rouge">Red Hat Enterprise Linux</code>:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/uefi_boot_mgrB.png" alt="Select RHEL" />
  <figcaption>Select RHEL</figcaption>
</figure>
</div>

<h1 id="provisioning-over-the-network-with-satellite">Provisioning over the Network with Satellite</h1>

<p>So the installation procedure above works, but is a bit labor
intensive and I’d rather not do it again, especially if I build this
out to a full 16 Raspberry Pis!</p>

<p>At this junction, I can’t automate everything.  Notably, installing
the UEFI firmware and partitioning the SD Card will have to be done by
hand.</p>

<p>However, I can install and reinstall RHEL over and over using a very
similar process to how I provision x86_64 RHEL Machines.</p>

<p>The provisioning part of this setup is very likely not supported by
Red Hat.  However, other portions, like hosting ARM64 content in
Satellite is.  Since this is my home lab, I don’t care about support,
but still caveat emptor.</p>

<p>This section assumes an already working Satellite provisioning setup
for x86_64 servers!</p>

<h2 id="importing-the-rhel-content-into-satellite">Importing the RHEL Content into Satellite</h2>

<p>The first step here, is getting our content into Satellite.  This is
relatively straightforward and the ARM architecture follows the
standard for RHEL.  We have a AppStream and BaseOS repositories, as
well as Kickstart repositories.</p>

<p>Kickstart repositories are designed for provisioning and are tied to a
specific RHEL release, e.g. 9.4, 10.1, et cetera.</p>

<p>The normal AppStream and BaseOS repositories are rolling releases, and
are used post-provisioning for patching and installing new software.</p>

<p>These repositories need to be synced to the Satellite, added to
a Content View, and then, optionally, that Content View promoted to a
Lifecycle Environment.</p>

<p>All this setup, is really beyond the scope of this post, but below are
the <code class="language-plaintext highlighter-rouge">hammer</code> commands to create a Content View.  In my environment, I
have one Lifecycle Environment, named Production, and the below
commands will promote the new content view in to that Lifecycle
Environment and create a corresponding Activation Key.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>### Setting a shell variable for the organization id.  This will
### likely be one but will be dependent on your installation.

# ORG_ID=1

### Enable the various Repository sets for RHEL.  Not all of these are
### strictly needed.
###
### Note the Kickstart repositories need to be a specific version,
### 9.7, whereas most other repositories just use the rolling 9
### release.

# hammer repository-set enable --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux 9 for ARM 64 - BaseOS (Kickstart)" --basearch aarch64 --releasever 9.7
# hammer repository-set enable --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux 9 for ARM 64 - AppStream (Kickstart)" --basearch aarch64 --releasever 9.7
# hammer repository-set enable --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux 9 for ARM 64 - AppStream (RPMs)" --basearch aarch64 --releasever 9
# hammer repository-set enable --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux 9 for ARM 64 - BaseOS (RPMs)" --basearch aarch64 --releasever 9
# hammer repository-set enable --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux 9 for ARM 64 - Supplementary (RPMs)" --basearch aarch64 --releasever 9
# hammer repository-set enable --organization-id ${ORG_ID} --name "Red Hat Satellite Client 6 for RHEL 9 aarch64 (RPMs)" --basearch aarch64


### Synchronize all this content down to the Satellite server.

# hammer product synchronize --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux for ARM 64"

### Optionally add this product to the daily sync plan.  I think this
### sync plan is the default now for Satellite, but I'm not sure.

# hammer product set-sync-plan --organization-id ${ORG_ID} --name "Red Hat Enterprise Linux for ARM 64" --sync-plan "Daily Sync"


### Generate a list of repositories.  This command will generate a
### correct list ONLY if this is the first time adding any ARM64
### products/repositories to the Satellite.

# REPOS=$(hammer repository list --organization-id ${ORG_ID} | egrep 'aarch64' | awk '{print $1","}' | tr -d '\n' | sed -e 's/,$//')

### Create the Content View

# hammer content-view create --organization-id ${ORG_ID} --name "RHEL 9 ARM64" --repository-ids ${REPOS}

### Publish the Content View

# hammer content-view publish --organization-id ${ORG_ID} --name "RHEL 9 ARM64"

### Promote the Content View

# hammer content-view version promote  --content-view "RHEL 9 ARM64" --organization-id ${ORG_ID} --to-lifecycle-environment "Production"

### Create Activation Key.  Since I'm using Simple Content Access
### mode, I don't have to attach a subscription.

# hammer activation-key create --name RHEL9_ARM64 --organization-id ${ORG_ID} --content-view "RHEL 9 ARM64" --lifecycle-environment "Production"
</code></pre></div></div>

<p>After all those <code class="language-plaintext highlighter-rouge">hammer</code> commands, I have all the content-related
parts of the Satellite configuration done.</p>

<h2 id="satellite-provisioning-flow">Satellite Provisioning Flow</h2>

<p>In Satellite, host provisioning is generally “network-driven”.  What
that means is that the host <em>always</em> boots from the network and
Satellite serves different network boot files to influence the behavior
of the host.</p>

<p>So, when Satellite is provisioning the host, i.e. it’s listed in build
status in Satellite, the network boot files will be generated such
that those file contain configuration that instructs the host to run
the installer and use a Satellite generated kickstart file.</p>

<p>Once provisioning is finished, then those network boot files are
modified to instruct the host to boot off local media (e.g. the SD
Card or attached USB or SATA drive).</p>

<p>By managing hosts this way, I can re-provision RHEL by just clicking a
button in Satellite and rebooting the host.</p>

<p>There are several challenges to getting this to work.</p>

<h2 id="first-challenge--dhcp-booting">First Challenge:  DHCP Booting</h2>

<p>The first challenge is getting the DHCP server to send ARM-specific
boot files to ARM servers.</p>

<p>By default, the configuration for Satellite’s built-in ISC-DHCP
server only includes logic for handling x86_64 and x86 machines.</p>

<p>When a DHCP client requests and receives a lease, there are a large
number of DHCP options that can be passed to augment the base
protocol.  These options are defined by
<a href="https://www.rfc-editor.org/rfc/rfc2939.html">RFC2939</a> and the IANA
maintains a list of <a href="https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml">DHCP
Options</a>.
Option 93, Client System Architecture, is the relevant option for booting.
Option 93 is defined by
<a href="https://www.rfc-editor.org/rfc/rfc4578.html">RFC4578</a> and, again, the
IANA maintains a current list of <a href="https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#processor-architecture">Processor Architecture
Types</a>.
Interestingly, this list is for DHCPv6 and I could not find one for
IPv4 DHCP, but this list appears to be accurate for both variants of
DHCP.</p>

<p>The IANA Processor Architecture Types list defines <code class="language-plaintext highlighter-rouge">0x06</code> for
x86/i386, <code class="language-plaintext highlighter-rouge">0x07</code> for x86_64/amd64, <code class="language-plaintext highlighter-rouge">0x09</code> for EBC (EFI byte code, a
generic EFI type), and <code class="language-plaintext highlighter-rouge">0x0b</code> for ARM64.  These values are for
traditional tftp-based netbooting, which I will be using.  There are a
separate set of codes for http-based netbooting.</p>

<p>In the default <code class="language-plaintext highlighter-rouge">/etc/dhcp/dhcpd.conf</code> file, there is an if/else block
that controls this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>option architecture code 93 = unsigned integer 16 ;
if option architecture = 00:06 {
  filename "grub2/shim.efi";
} elsif option architecture = 00:07 {
  filename "grub2/shim.efi";
} elsif option architecture = 00:09 {
  filename "grub2/shim.efi";
} else {
  filename "pxelinux.0";
}
</code></pre></div></div>

<p>These filenames are based on a relative path to the TFTP root
directory.  In the case of RHEL/Satellite, the default is
<code class="language-plaintext highlighter-rouge">/var/lib/tftpboot</code></p>

<p>Based on this, clients with architecture code <code class="language-plaintext highlighter-rouge">0x0b</code> fall through to
the last <code class="language-plaintext highlighter-rouge">else</code> block and will be sent <code class="language-plaintext highlighter-rouge">/var/lib/tftpboot/pxelinux.0</code>,
which is <em>wrong</em>.  pxelinux.0 is an x86 executable for traditional
BIOS-style PXE boot.</p>

<p>I’ll need to add a stanza for the ARM64 architecture.  But before we
can do that, we need to get the appropriate files and place them in
<code class="language-plaintext highlighter-rouge">/var/lib/tftpboot</code>.</p>

<h2 id="second-challenge--getting-grub2-and-uefi-shim-for-arm64">Second Challenge:  Getting Grub2 and UEFI Shim for ARM64</h2>

<p>For UEFI booting on i386 and x86_64, there are two main boot executables that are used:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">shim.efi</code></li>
  <li><code class="language-plaintext highlighter-rouge">grub$(arch).efi</code></li>
</ol>

<p><code class="language-plaintext highlighter-rouge">shim.efi</code> is a signed boot executable for use with computers
requiring SecureBoot.  These kinds of machines require a
cryptographically signed executable the first boot executable.</p>

<p><code class="language-plaintext highlighter-rouge">grub$(arch).efi</code> is a copy of the <a href="https://www.gnu.org/software/grub/">Grand Unified Boot
Loader</a>.  GRUB is responsible for
loading the Linux kernel and actually starting the Linux boot process.</p>

<p><code class="language-plaintext highlighter-rouge">shim.efi</code> is loaded first, and then it can then load subsequent
executables.  It immediately tries find and load a corresponding GRUB
executable, attempting a list of potential file names including
<code class="language-plaintext highlighter-rouge">grub.efi</code>, <code class="language-plaintext highlighter-rouge">grubx64.efi</code> and others.</p>

<p>The UEFI firmware on Raspberry Pi’s do not require SecureBoot, so a
equivalent <code class="language-plaintext highlighter-rouge">shim.efi</code> is not needed, but I’ll use it anyway just to
keep things consistent.  Since I’m hosting both x86_64 and ARM64 on
the same Satellite server, the architecture code (aa64 for ARM, x64
for x86_64) needs to be included in the filename.  Thus, the files for
ARM will be named <code class="language-plaintext highlighter-rouge">shimaa64.efi</code> and <code class="language-plaintext highlighter-rouge">grubaa64.efi</code>.</p>

<p>Where does one obtain these files?  They are contained in 3 packages,
and are also installed by default in any running ARM installation.
In the list below, <code class="language-plaintext highlighter-rouge">grub2-efi-aa64-modules</code> is not needed for initial
boot and install, but will become important later.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">shim-aa64</code></li>
  <li><code class="language-plaintext highlighter-rouge">grub2-efi-aa64</code></li>
  <li><code class="language-plaintext highlighter-rouge">grub2-efi-aa64-modules</code></li>
</ol>

<p>However, if I try to install these on my Satellite Server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># dnf install -y grub2-efi-aa64
Updating Subscription Management repositories.
Last metadata expiration check: 2:01:45 ago on Mon 08 Dec 2025 02:31:14 PM EST.
No match for argument: grub2-efi-aa64
Error: Unable to find a match: grub2-efi-aa64
</code></pre></div></div>

<p>These packages (except <code class="language-plaintext highlighter-rouge">grub2-efi-aa64-modules</code>) are not available in
the x86_64 RHEL repositories.</p>

<p>So, that leaves a few methods to get them:</p>
<ol>
  <li>Download the RPMs from <code class="language-plaintext highlighter-rouge">access.redhat.com</code> and extract what we need</li>
  <li>Get the RPMs from an installation ISO</li>
  <li>Get the files from a running system.</li>
</ol>

<p>Since I had already done a test install of RHEL from a USB Stick, I
chose option #3.  I’ll leave the two others as an exercise to the
reader.</p>

<p>My test host has a hostname of <code class="language-plaintext highlighter-rouge">rpi1.private.opequon.net</code>. First, I
ensure that all three of the above packages are installed and then use
the following commands will synchronize all the appropriate files:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># # ON SATELLITE SERVER
# cd /var/lib/tftpboot/

# # Copy files from grub2-efi-aa64-modules
# rsync -avz --delete root@rpi1.private.opequon.net:/usr/lib/grub/arm64-efi .
# chown -R foreman-proxy:root arm64-efi

# # Copy files from grub2-efi-aa64 and shim-aa64
# scp root@rpi1.private.opequon.net:/boot/efi/EFI/redhat/*aa64*.efi .
root@rpi1.private.opequon.net's password: 
grubaa64.efi                                                                  100% 2622KB  20.0MB/s   00:00    
mmaa64.efi                                                                    100%  873KB   9.4MB/s   00:00    
shimaa64-redhat.efi                                                           100%  956KB  10.0MB/s   00:00    
shimaa64.efi                                                                  100%  956KB   9.9MB/s   00:00    
# chown foreman-proxy:root *aa64*.efi
# chmod 0644 *aa64*.efi
# ls -la *aa64*.efi
-rw-r--r--. 1 foreman-proxy root 2684536 Dec  8 16:43 grubaa64.efi
-rw-r--r--. 1 foreman-proxy root  893760 Dec  8 16:43 mmaa64.efi
-rw-r--r--. 1 foreman-proxy root  978528 Dec  8 16:43 shimaa64.efi
-rw-r--r--. 1 foreman-proxy root  978528 Dec  8 16:43 shimaa64-redhat.efi
</code></pre></div></div>

<p>This copies a few unnecessary files like <code class="language-plaintext highlighter-rouge">mmaa64.efi</code>, which is a
memory tester, and <code class="language-plaintext highlighter-rouge">shimaa64-redhat.efi</code> which has the same contents
as <code class="language-plaintext highlighter-rouge">shimaa64.efi</code>, but it’s not a big deal.</p>

<p>Now with these in place, we can update our <code class="language-plaintext highlighter-rouge">/etc/dhcp/dhcpd.conf</code> to
include a reference to these executables:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>option architecture code 93 = unsigned integer 16 ;
if option architecture = 00:06 {
  filename "grub2/shim.efi";
} elsif option architecture = 00:07 {
  filename "grub2/shim.efi";
} elsif option architecture = 00:09 {
  filename "grub2/shim.efi";
} elsif option architecture = 00:0b {
  filename "grub2/shimaa64.efi";
} else {
  filename "pxelinux.0";
}
</code></pre></div></div>

<p>Unfortunately, this configuration is <strong>NOT</strong> permanent!  <code class="language-plaintext highlighter-rouge">dhcpd.conf</code>
is under control of Satellite’s puppet modules and will get rewritten
next time that <code class="language-plaintext highlighter-rouge">satellite-maintain</code> is run.  However, fixing that is a
tomorrow problem.</p>

<h2 id="third-challenge--loading-grub-modules">Third Challenge:  Loading GRUB Modules</h2>

<p>With the DHCP changes and efi files in place, I can run an
installation.  However, once the installation is complete, the system
doesn’t come up on reboot!  There are errors about chainloading.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/grub_errors.png" alt="Grub Chainloading Errors" />
  <figcaption>Grub Chainloading Errors</figcaption>
</figure>
</div>

<p>Now this doesn’t mean that the installation wasn’t successful!  If I
were to navigate through the UEFI console and boot from the <code class="language-plaintext highlighter-rouge">Red Hat
Enterprise Linux</code> <code class="language-plaintext highlighter-rouge">Boot Menu</code> entry, then the machine comes up
successfully.</p>

<p>So, what’s going on here?</p>

<p>Recall that Satellite is controlling the boot process through network
booting configuration files.  At the end of the install process, there
is a call-back to the Satellite server that indicates the installation
has completed.  This causes the Satellite server to change the net
boot files to instruct the host to boot from local disk.</p>

<p>The DHCP server tells the host to load <code class="language-plaintext highlighter-rouge">shimaa64.efi</code>, which then
loads <code class="language-plaintext highlighter-rouge">grubaa64.efi</code>.  GRUB looks for configuration and, by default,
it will look in the same place that the <code class="language-plaintext highlighter-rouge">grubaa64.efi</code> came from: the
TFTP server.</p>

<p>On the TFTP server, we can find our GRUB configuration under
<code class="language-plaintext highlighter-rouge">/var/lib/tftpboot/grub2/</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cd /var/lib/tftpboot/grub2
# ls -la

###
### I've cut out a lot of files here for clarity/brevity
### 

-rw-r--r--.  1 foreman-proxy root              320 Jan 20  2021 grub.cfg
-rw-r--r--.  1 foreman-proxy foreman-proxy    1771 Jan 20 23:30 grub.cfg-01-d8-3a-dd-f2-ee-50
-rw-r--r--.  1 foreman-proxy foreman-proxy    1771 Jan 20 23:30 grub.cfg-d8:3a:dd:f2:ee:50
</code></pre></div></div>

<p>GRUB will go through different file patterns to find the correct one.
In Satellite, the Ethernet MAC address is used to uniquely tie a
specific file to GRUB.  In the above example, there are two different
variations of filenames for a machine with MAC address
<code class="language-plaintext highlighter-rouge">d8:3a:dd:f2:ee:50</code>.  I’m not sure which one GRUB, but Satellite
generates them both.</p>

<p>After GRUB runs through all it’s filename patterns, it will default to
grub.cfg, but in a Satellite provisioning situation, that should never
happen.</p>

<p>These files tend to be fairly complicated scripts, and the ones
provided by Satellite are provided as templates that are heavily
generalized to work with RHEL, Fedora and several other distributions.</p>

<p>There are, of course different templates depending on what action is
expected from the host.  In this case, since the installation was
completed, Satellite is expecting the host to boot from local boot, so
the <code class="language-plaintext highlighter-rouge">PXEGrub2 default local boot</code> template was used.</p>

<p>While I’m troubleshooting, I can directly modify these file in
<code class="language-plaintext highlighter-rouge">/var/lib/tftpboot</code>.</p>

<p>A simplified version looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
# Loading modules for GPT Partitions, FAT Filesystem and Chainloading
insmod part_gpt
insmod fat
insmod chain

# Menu Entry to display
menuentry 'Chainload Grub2 EFI from ESP' --id local_chain_hd0 {
  
  # Search for the file to chainload
  unset chroot
  search --file --no-floppy --set=chroot /EFI/fedora/shim.efi

  # If this file is found, chainload it
  if [ -f ($chroot)/EFI/fedora/shim.efi ]; then
    chainloader ($chroot)/EFI/fedora/shim.efi
    echo "Found /EFI/fedora/shim.efi at $chroot, attempting to chainboot it..."
    sleep 2
    boot
  fi
</code></pre></div></div>

<p>“Chainloading” in this case is using the copy of GRUB we pulled down
from the network to load another copy of GRUB (or if it were a
different operating system, then loading that operating system’s
bootloader).  This second copy of grub then is used to load the RHEL
installation on the local storage.</p>

<p>Chainloading (and the chainloader command) is <em>NOT</em> part of the basic
<code class="language-plaintext highlighter-rouge">grub.efi</code> executable, at least so far as ARM is concerned.
Therefore, that functionality needs to be loaded in at runtime from a
module.  This is the intent of the <code class="language-plaintext highlighter-rouge">insmod chain</code> command.</p>

<p>For some reason, the <code class="language-plaintext highlighter-rouge">insmod</code> commands fails to find the module to
load when running on ARM.  I’m not sure exactly how it works on
x86_64.  Debugging here is difficult.  The chainload module is either
part of the base <code class="language-plaintext highlighter-rouge">grub.efi</code> for x86_64 or the insmod is somehow able
to figure out how to load it.  I’m not sure.</p>

<p>Regardless, I was able to figure out how to get <code class="language-plaintext highlighter-rouge">insmod</code> to work
correctly and load the ARM version of modules from Satellite’s tftp
server!  This requires changing the insmod command to include some
hints on where to find the actual module.</p>

<p>From:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>insmod chain
</code></pre></div></div>

<p>change to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>insmod (tftp)grub2/arm64-efi/chain.mod
</code></pre></div></div>

<p>Similar changes can be made to the other insmod statements, but
<code class="language-plaintext highlighter-rouge">chain.mod</code> was the one causing the issue.</p>

<p>These hints tell GRUB to load this module from the TFTP server from a
specific directory.  Now that it can successfully get this module, the
boot can continue!</p>

<h2 id="fourth-challenge---post-install-template">Fourth Challenge:   Post-Install Template</h2>

<p>Modifying GRUB files in the <code class="language-plaintext highlighter-rouge">/var/lib/tftpboot</code> directory works, but
Satellite owns these files and will rewrite these for various reasons.
I need to modify the Satellite template used to generate this file.</p>

<p>Satellite uses the template referenced by the <code class="language-plaintext highlighter-rouge">localboot template</code>
parameter to generate the post installation version of this GRUB
configuration.  Unfortunately, this parameter is not exposed in the UI
except for at a global level.  Since this variant is ONLY applicable
to ARM, I don’t want to override this template FOR EVERY MACHINE.</p>

<p>To solve this, I can use Satellite Host Groups functionality to create
a Host Group just for Raspberry Pi hardware.  The Host Group will have
a parameter set to reference the updated templates for localboot.</p>

<h2 id="putting-it-all-together--satellite-configuration">Putting it all Together:  Satellite Configuration</h2>

<p>Working through those challenges, I can now have all the information I
need to configure Satellite to provision a host.</p>

<ol>
  <li>Create a new templates for ARM including a new localboot template and a partition table</li>
  <li>Create a hostgroup using the Content View, Partition Table, and Local Boot template</li>
  <li>Ensure my Rapsberry Pis are set to boot from the network first</li>
  <li>Create a host in that host group to provision</li>
  <li>Reboot and watch it run!</li>
</ol>

<h3 id="putting-it-all-together---satellite-templates">Putting it all Together:   Satellite Templates</h3>

<p>To complete the configuration, I had to create two templates in
Satellite.</p>

<ol>
  <li>Chainload Template</li>
</ol>

<p>I cloned <code class="language-plaintext highlighter-rouge">pxegrub2_chainload</code> template into <code class="language-plaintext highlighter-rouge">pxegrub2_chainload_RPI</code>
and modified it to include <code class="language-plaintext highlighter-rouge">insmod</code> changes.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;%#
kind: snippet
name: pxegrub2_chainload
model: ProvisioningTemplate
snippet: true
description: |
  In Foreman's typical PXE workflow, managed hosts are configured to always boot from network and inventory build flag dictates if they should boot into installer (build is on) or boot from local drive (build is off). This template is used to chainload from EFI ESP for systems which booted from network. It is not as straightforward as in BIOS and EFI boot file must be found on an ESP partition.

  This will only be needed when provisioned hosts are set to boot from network, typically EFI firmware implementations overrides boot order after new OS installation. This behavior can be set in EFI, or "efi_bootentry" host parameter can be set to "previous" to override boot order back to previous (network) setting. See efibootmgr_netboot snippet for more info.
-%&gt;
&lt;%
  paths = [
    '/EFI/redhat/shimaa64.efi',
    '/EFI/redhat/grubaa64.efi'
  ]
  config_paths = [
    '/EFI/fedora/grub.cfg',
    '/EFI/redhat/grub.cfg',
    '/EFI/centos/grub.cfg',
    '/EFI/rocky/grub.cfg',
    '/EFI/almalinux/grub.cfg',
    '/EFI/debian/grub.cfg',
    '/EFI/ubuntu/grub.cfg',
    '/EFI/sles/grub.cfg',
    '/EFI/opensuse/grub.cfg',
  ]
-%&gt;

insmod (tftp)grub2/arm64-efi/part_gpt.mod
insmod (tftp)grub2/arm64-efi/fat.mod
insmod (tftp)grub2/arm64-efi/chain.mod


&lt;%=
  default_connectefi_option = 'scsi'
  connectefi_option = @host ? host_param('grub2-connectefi', default_connectefi_option) : default_connectefi_option
  connectefi_option = nil if connectefi_option == 'false'
  "connectefi #{connectefi_option}" if connectefi_option
%&gt;


menuentry 'Chainload Grub2 EFI from ESP' --id local_chain_hd0 {
  echo "Chainloading Grub2 EFI from ESP, enabled devices for booting:"
  ls
&lt;%
  paths.each do |path|
-%&gt;
  echo "Trying &lt;%= path %&gt; "
  unset chroot
  # add --efidisk-only when using Software RAID
  search --file --no-floppy --set=chroot &lt;%= path %&gt;
  if [ -f ($chroot)&lt;%= path %&gt; ]; then
    chainloader ($chroot)&lt;%= path %&gt;
    echo "Found &lt;%= path %&gt; at $chroot, attempting to chainboot it..."
    sleep 2
    boot
  fi
&lt;%
  end
-%&gt;
  echo "Partition with known EFI file not found, you may want to drop to grub shell"
  echo "and investigate available files updating 'pxegrub2_chainload' template and"
  echo "the list of known filepaths for probing. Available devices are:"
  echo
  ls
  echo
  echo "If you cannot see the HDD, make sure the drive is marked as bootable in EFI and"
  echo "not hidden. Boot order must be the following:"
  echo "1) NETWORK"
  echo "2) HDD"
  echo
  echo "The system will poweroff in 2 minutes or press ESC to poweroff immediately."
  sleep -i 120
  halt
}
</code></pre></div></div>

<ol>
  <li>local boot template</li>
</ol>

<p>The <code class="language-plaintext highlighter-rouge">pxegrub2_chainload_RPI</code> template above is a snippet.  The
original one was used by the <code class="language-plaintext highlighter-rouge">PXEGrub2 default local boot</code> template.</p>

<p>I cloned the <code class="language-plaintext highlighter-rouge">PXEGrub2 default local boot</code> template into <code class="language-plaintext highlighter-rouge">PXEGrub2
default local boot clone RPI</code> and modified to use my newly created
<code class="language-plaintext highlighter-rouge">pxegrub2_chainload_RPI</code> template.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;%#
kind: PXEGrub2
name: PXEGrub2 default local boot
model: ProvisioningTemplate
description: |
  The template to render Grub2 bootloader configuration for provisioned hosts,
  that still boot from the network.
  Hosts are instructed to boot from the first local medium.
  Do not associate or change the name.
-%&gt;
set default=&lt;%= global_setting("default_pxe_item_local", "local") %&gt;
set timeout=20
echo Default PXE local template entry is set to '&lt;%= global_setting("default_pxe_item_local", "local") %&gt;'

&lt;%= snippet "pxegrub2_chainload_RPI" %&gt;
</code></pre></div></div>

<ol>
  <li>Partition table</li>
</ol>

<p>Using the kickstart file generated when I installed with a USB drive,
I created a partition table template named <code class="language-plaintext highlighter-rouge">RPi SD-Card and SATA Drive
Default</code>.</p>

<p>In order for the Partition Table to show up in <code class="language-plaintext highlighter-rouge">Create Host</code> or
<code class="language-plaintext highlighter-rouge">Create Host Group</code> screens, it must be associated with an individual
Operating System version.</p>

<p>Since I added the RHEL 9.7 Kickstart, I need to add this template
specifically to the Red Hat 9.7/RHEL 9.7 Operating System.</p>

<p>This is found under the <code class="language-plaintext highlighter-rouge">Hosts-&gt;Provisioning Setup-&gt;Operating System</code>
menu item:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/os-part-map1.png" alt="Operating System Menu" />
  <figcaption>Operating System Menu</figcaption>
</figure>
</div>

<p>In Partition Table tab for the specific RHEL Version (in our case
RedHat 9.7):</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/os-part-map2.png" alt="Operating System Partition Table selection Menu" />
  <figcaption>Operating System Partition Table selection Menu</figcaption>
</figure>
</div>

<p>Make sure the partition table template is in the selected items
column!</p>

<p>The contents of the partition table template are:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Default RPi to use only attached SD Card

# Generated using Blivet version 3.6.0
ignoredisk --only-use=mmcblk1,sda
# System bootloader configuration
bootloader --append="crashkernel=1G-4G:256M,4G-64G:320M,64G-:576M" --location=mbr --boot-drive=mmcblk1
# Partition clearing information
zerombr
clearpart --linux 
# Disk partitioning information
part /boot/efi --fstype="efi" --noformat --onpart=mmcblk1p1 --fsoptions="umask=0077,shortname=winnt"
part /boot --fstype="xfs" --ondisk=mmcblk1 --size=1024
part / --fstype="xfs" --ondisk=mmcblk1 --size=20480 --grow

part pv.1 --fstype="lvmpv" --ondisk=sda --grow
volgroup rhel_rpi --pesize=4096 pv.1
logvol swap --fstype="swap" --size=4092 --name=swap --vgname=rhel_rpi
</code></pre></div></div>

<p>I decided on putting <code class="language-plaintext highlighter-rouge">/boot</code> and <code class="language-plaintext highlighter-rouge">/</code> on the SD Card as normal
partitions, rather than Logical Volumes.  Then on the SATA drive, I
put that drive’s entire contents into a volume group called
<code class="language-plaintext highlighter-rouge">rhel_rpi</code>.  In the above table, I only put a single logical volume,
one for swap.  However, in future, I’ll likely add one for <code class="language-plaintext highlighter-rouge">/var</code> and
other locations.</p>

<p>I’m not sure if this is the most optimal or not, but it does seem to
cause the least issues on provisioning/re-provisioning.</p>

<p>Normally, I would just have a kickstart that would wipe <em>ALL</em> the
drives and go from there.  This is not possible here, because <strong>the
EFI system partition</strong> must be preserved.</p>

<p>I had three scenarios I needed to cover:</p>

<ol>
  <li>
    <p>Only the EFI partition on the SD Card and no partition table on
the SATA drive. This is the “brand new” state.</p>
  </li>
  <li>
    <p>Multiple partitions on the SD Card, and no partition table on the
SATA drive.  This is unlikely to happen realistically, but could,
if I replaced the SATA drive.</p>
  </li>
  <li>
    <p>Multiple partitions on the SD Card, and partitions on the SATA
drive.  This is the normal re-provisioning states.</p>
  </li>
</ol>

<p>In the kickstart directive, the <code class="language-plaintext highlighter-rouge">clearpart --linux</code> was very useful.
It clears <strong>ONLY</strong> Linux related partitions, so it leaves the <code class="language-plaintext highlighter-rouge">EFI
System</code> partition unmodified and intact, but will delete any other
partitions.</p>

<p>This solves scenario #3 above, which is likely the most common, but
partitioning would still fail on #1 and #2 because there wasn’t a
partition table on the SATA drive.</p>

<p>I tried multiple arguments to <code class="language-plaintext highlighter-rouge">clearpart</code> to solve #1 and #2, but
ended up realizing that <code class="language-plaintext highlighter-rouge">zerombr</code> was what I actually needed.
<code class="language-plaintext highlighter-rouge">zerombr</code> initializes a partition table <em>only on disk that need it</em>.
So it effectively ignores the SD Card, since it will always have a
partition table, but will, if needed, put a partition table on the
SATA drive.</p>

<h3 id="putting-it-all-together---host-group">Putting it all together:   Host Group</h3>

<p>With those templates and provisioning templates, I can now create the
Host Group:</p>

<p>In the host group tab, I set the Lifecycle Environment, Content View,
and Deploy On.  In Content Source, <code class="language-plaintext highlighter-rouge">yavanna.private.opequon.net</code> is my
Satellite host.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/hostgroup1.png" alt="Host Group Tab" />
  <figcaption>Host Group Tab</figcaption>
</figure>
</div>

<p>In the Operating System tab, I select the RHEL Release that
corresponds with our Kickstart repository, update the Partition Table
to the template I created, and then set the PXE loader to Grub2 UEFI.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/hostgroup2.png" alt="Operating System Tab" />
  <figcaption>Operating System Tab</figcaption>
</figure>
</div>

<p>In the parameters tab, we add <code class="language-plaintext highlighter-rouge">kt_activation_keys</code> to the Activation
Key that was created and <code class="language-plaintext highlighter-rouge">local_boot_PXEGrub2</code> to the template we
created for local boot.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/hostgroup3.png" alt="Parameters Tab" />
  <figcaption>Parameters Tab</figcaption>
</figure>
</div>

<h3 id="putting-it-all-togther--changing-boot-order">Putting it all togther:  Changing Boot Order</h3>

<p>For this to work, a host must have <code class="language-plaintext highlighter-rouge">UEFI PXEv4</code> set as the default
boot option.  This is the default unless one has installed via a USB
drive or manually modified the boot order.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/changing_boot_order.png" alt="Boot Order Screen" />
  <figcaption>Boot Order Screen</figcaption>
</figure>
</div>

<h3 id="putting-it-all-together---creating-hosts">Putting it all together:   Creating Hosts</h3>

<p>Now that I’ve got everything in place, I can create hosts.</p>

<p>Navigate to Create Host in the Satellite Menu:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost1.png" alt="Create New Host" />
  <figcaption>Create New Host</figcaption>
</figure>
</div>

<p>Fill out a hostname and select the Host Group for the Raspberry Pis:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost2.png" alt="Create New Host, Host Tab " />
  <figcaption>Create New Host, Host Tab </figcaption>
</figure>
</div>

<p>The Host Group contains almost all the basic parameters are needed to
provision this host.  So, there’s nothing much else to enter:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost3.png" alt="Create New Host, Operating System Tab" />
  <figcaption>Create New Host, Operating System Tab</figcaption>
</figure>
</div>

<p>The Network Interface still needs to be defined:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost4.png" alt="Create New Host, Network Interface Dialog" />
  <figcaption>Create New Host, Network Interface Dialog</figcaption>
</figure>
</div>

<p>Like so:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost5.png" alt="Create New Host, Network Interface Tab" />
  <figcaption>Create New Host, Network Interface Tab</figcaption>
</figure>
</div>

<p>With the Host defined in Satellite, I can boot the Raspberry Pi and it should start to PXE boot:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost6.png" alt="Pi PXE Booting" />
  <figcaption>Pi PXE Booting</figcaption>
</figure>
</div>

<p>And then continue onto GRUB:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost7.png" alt="GRUB Bootloader" />
  <figcaption>GRUB Bootloader</figcaption>
</figure>
</div>

<p>And after some time, it should continue to installing:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/rpi4rhel/sat_newhost8.png" alt="Installing via Kickstart" />
  <figcaption>Installing via Kickstart</figcaption>
</figure>
</div>

<h2 id="summary">Summary</h2>

<p>After doing all this configuration in Satellite, I now have a very
similar process to provisioning these Raspberry Pis as I do my x86_64
machines.</p>

<p>This technically checks off my requirement #5 “Must be able to
provision over the network and ideally through Red Hat Satellite”.</p>

<p>However, I fear this setup is a bit fragile.  I know that the DHCPd
configuration will be overwritten next time I upgrade Satellite.  I’ve
started to look into patching Satellite, but it may be easier to run a
separate DHCP server that is not entirely managed by Satellite.</p>

<p>In the same vein, I’m not sure if the UEFI binaries put in
<code class="language-plaintext highlighter-rouge">/var/lib/tftpboot</code> will stay in place or not.  Upgrading Satellite
<em>may</em> wipe them out, but I’m not 100% sure yet.  Regardless, those
were setup by hand and not by any supported way.  So they wont be
updated and could potentially become out of date.</p>

<p>That being said, I’m pleased with how this came together, especially
for something unsupported.</p>

<h1 id="cost-calculations">Cost Calculations</h1>

<p>Prices are going to fluctuate, so this section will likely be out of date very quickly.   This analysis is current as of Jan 2026.</p>

<p>Each Raspberry Pi was configured identically:</p>

<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Raspberry Pi</td>
      <td>$74.99</td>
    </tr>
    <tr>
      <td>SD Card (32 GiB)</td>
      <td>$11.49</td>
    </tr>
    <tr>
      <td>SSD (120GiB)</td>
      <td>$23.99</td>
    </tr>
    <tr>
      <td>PoE Hat</td>
      <td>$24.99</td>
    </tr>
    <tr>
      <td>HDMI Cable</td>
      <td>$10.99</td>
    </tr>
    <tr>
      <td>Total</td>
      <td>$146.45</td>
    </tr>
  </tbody>
</table>

<p>The UCTronics Chassis cost $269.99, I divided by 4 to get the per Pi price of $21.87.</p>

<p>The TESMart KVM switch cost $349.99.  I divided that by 16 to get a per Pi price of $6.25.</p>

<p>I haven’t bought a NanoKVM or equivalent for this system yet, but I put in a price of $100 or $6.25 per Pi.</p>

<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Raspberry Pi + accessories</td>
      <td>$146.45</td>
    </tr>
    <tr>
      <td>Chassis (per Pi cost)</td>
      <td>$67.50</td>
    </tr>
    <tr>
      <td>KVM Switch (per Pi Cost)</td>
      <td>$21.87</td>
    </tr>
    <tr>
      <td>IP KVM (per Pi Cost)</td>
      <td>$6.25</td>
    </tr>
    <tr>
      <td>TOTAL per Pi Cost</td>
      <td>$242.07</td>
    </tr>
  </tbody>
</table>

<p>Maxing this configuration out with 16 total Pis yields an estimated
cost of $3873.12.</p>

<p>This is not including the price of a PoE-compatible switch!</p>

<p>Yikes!</p>

<p>If one is comparing with <em>NEW</em> x86_64 servers or even refurbished
recent models (e.g. a HPE Gen 10+), then this is cheaper.  Gathering
quotes on <a href="https://www.servermonkey.com/">Server Monkey</a>, a refurbished
HPE DL360 Gen 10 Plus chassis (without processors, RAM, or disk)
starts at $3,100.</p>

<p>However, the if I go back to slightly older models, a HPE Gen 10 DL360
w/ a 16 Core Processor, 128 GiB RAM, and 4 TB SATA SSD can be priced out
for upper $2,000s to low $3,000s.  I feel pretty confidant that a
single server configured like that could run at least 16 virtual
machines if not more and be the similar, if not <em>more</em> performant.</p>

<p>On the other hand, 16 independant hosts have a lower blast-radius for
failure compared to a single machine.  So that price for the HPE
server is maybe low.  I’d want more than 1 drive for redundancy, since
a single drive failure would wipe out ALL my virtual machines. And,
I’d probably want more than 128 GiB of RAM to account for operating
system overhead.</p>

<p>Server Monkey is also not the cheapest vendor.  I would likely be able
to find a better deal on eBay or somewhere else, eventually.  This is
how I’ve acquired most of my server hardware anyhow, but it requires
patience and diligence.</p>

<p>That being said, there’s also some flexibility in the price of the Pi
setup.  I think I overpaid for HDMI cables and SD Cards….and I
certainly feel the UCTronics chassis is over priced.</p>

<p>I also may be able to get more value, though not necessarily a lower
price, through buying more capable single board computers, like
Raspberry Pi 5s or Orange Pis.  There’s also potential experiments I
could do with even more unusual architectures like RISC V.</p>

<p>I’ll leave it up to the reader to determine if this is valuable or
not, but it’s certainly no slam dunk.</p>

<h1 id="problems-with-rhel">Problems with RHEL</h1>

<p>I encountered during this two bugs in RHEL itself that hampered progress.</p>

<p>The first occurred between RHEL 9.2 and 9.4, where a bug caused the
frame buffer to fail.  So the host would boot successfully, but I’d
have no video from the console.</p>

<p>This bug was fixed somewhere between 9.4 and 9.6.</p>

<p>The second bug involved the UEFI shim and other EFI modules.  The ones
from 9.7 work great.  However, after 9.7 they were updated and booting
fails when you have more than 3 GiB of RAM enabled.</p>

<p>I can copy the UEFI boot programs from 9.7 and they work as expected.
I’m sure I could track down why in the RPM, but I haven’t yet.</p>

<p>In the interim, I’ll likely have to black-list or version restrict the
UEFI packages.  Until I figure out which specific package, I can set
the <code class="language-plaintext highlighter-rouge">package_upgrade</code> parameter to <code class="language-plaintext highlighter-rouge">false</code> on the Host or the Host
Group in Satellite, which will prevent a <code class="language-plaintext highlighter-rouge">dnf update -y</code> from running
during the kickstart install.  Hopefully this is fixed when 9.8 is
released.</p>

<h1 id="conclusions-and-next-steps">Conclusions and next steps</h1>

<p>I am fairly happy with how this turned out…all my requirements were
fulfilled.  However, I’m not sure of the value here.  Whether a setup
like this is more cost effective than used x86_64 hardware is not
clear, but I’m leaning towards no.</p>

<p>That being said, initial cost isn’t the only factor.  I <em>do</em> believe
this setup is less noisy just from my own perception, though real
measurements are necessary.  I’ve also yet to do power consumption
tests, which may prove that TCO over a long period is better.</p>

<p>There are also some serious problems with this setup:</p>

<p>First, this isn’t really supported by Red Hat.  Both the Satellite
procedure as well as just running RHEL on Raspberry Pis.  I don’t care
too much as this is just lab hardware, <em>but</em> it is annoying that
anything could break with any update.</p>

<p>Second, while modifications to files like <code class="language-plaintext highlighter-rouge">/etc/dhcp/dhcpd.conf</code> are
functional, these files are controlled by Puppet modules.  Thus, the
changes will be overwritten when doing patching, upgrading, or
reconfiguration via <code class="language-plaintext highlighter-rouge">satellite-maintain</code>.</p>

<p>This is solvable, but may lead me to running a DHCP server separate
from Satellite.</p>

<p>Third, I’ve not been able to get RHEL 10 working…yet.  While still
current for quite some time, RHEL 9 is no longer the latest release.
I like to stay current if I can.</p>

<p>As for next steps, I’d like to do a few different things:</p>

<ol>
  <li>
    <p>Perform some power usage comparisions between this total setup and
some of my x86_64 servers.</p>
  </li>
  <li>
    <p>Integrate a NanoKVM or similar with the KVM switch.  This will
require some custom development work, but would be nice to be able
to switch between Raspberry Pis in the same interface.</p>
  </li>
  <li>
    <p>Integrate Power Controls into Satellite.  I can control things via
my PoE Switch, but it would be nice to take advantage of the
Satellite UI for that.</p>
  </li>
  <li>
    <p>Test this out with other RHEL variants, like CoreOS for OpenShift
Nodes.  I don’t think these would be powerful enough to run as
Control/Master nodes of K8s cluster…I suspect that the disk
latency is not good enough to run etcd, but it would be
interesting to see these Raspberry Pis be made part of a
multi-architecture OpenShift cluster.</p>
  </li>
  <li>
    <p>Test out Raspberry Pi 5’s and potentially other single-board
computers.</p>
  </li>
</ol>

<p>We’ll see if I have time for any of it!</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="linuxunix" /><category term="hardware" /><category term="linux" /><category term="rhel" /><category term="arm" /><category term="rhel" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">K8s PVs, NFS and ACLs and avoiding umask changes.</title><link href="/blog/k8s/2024/01/19/K8s-PVs-NFSv4-ACLs.html" rel="alternate" type="text/html" title="K8s PVs, NFS and ACLs and avoiding umask changes." /><published>2024-01-19T11:59:00-05:00</published><updated>2024-01-19T11:59:00-05:00</updated><id>/blog/k8s/2024/01/19/K8s-PVs-NFSv4-ACLs</id><content type="html" xml:base="/blog/k8s/2024/01/19/K8s-PVs-NFSv4-ACLs.html"><![CDATA[<p>I ran into an issue recently with my Tekton Pipelines with
PersistentVolume Permissions.</p>

<p>I needed my pods to create files with the group write bit set.
Essentially, I needed a umask of 0002, but my pods were running with
umask of 0022.</p>

<p>I was afraid I was going to have start twiddling around with umasks
for my Tekton tasks, but instead found that I could solve my issue
using Linux Access Control Lists (ACLs).</p>

<p>ACLs are not often used on Linux, but can be quite powerful.  For this
article, the relevant feature is the ability to set default
permissions for created files.  These ignore the current processes
umask and act as a directory specific umask.</p>

<h1 id="the-setup">The Setup</h1>

<p>My PersistentVolumes were NFSv4 shares exported from a RHEL8 host, so I
would need to set these ACLs on directories exported from that server.</p>

<p>Here is the normal behavior:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nfs-server#  cd /path/to/nfs_share

nfs-server# umask
0022      &lt;-  Files should be created rwxr-x-r-x 

nfs-server# getfacl .  
# file: .
# owner: root
# group: root
# flags: -s-
user::rwx
group::rwx
other::r-x


nfs-server# touch without_default_perms_acl
# ls -l
total 0
-rw-r--r--.  1 root root   0 Jan 19 11:44 without_default_perms_acl
</code></pre></div></div>

<p>Now, let me set default permissions via an ACL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nfs-server# cd /path/to/nfs_share

# Let's set our default ACLs for group and other.

nfs-server# setfacl -m d:g::rwX . 
nfs-server# setfacl -m d:o::r-X .

nfs-server# getfacl .
# file: .
# owner: root
# group: root
# flags: -s-
user::rwx
group::rwx
other::r-x
default:user::rwx
default:group::rwx
default:other::r-x

nfs-server# umask
0022      &lt;-  Our umask remains unchanged

nfs-server# touch with_default_perms_acl
nfs-server# ls -l 
total 0
-rw-rw-r--. 1 root root 0 Jan 19 11:50 with_default_perms_acl
-rw-r--r--. 1 root root 0 Jan 19 11:44 without_default_perms_acl
</code></pre></div></div>

<p>As you can see, the file created AFTER setting the default ACLs
ignores the umask and instead uses the default set in the ACL!
New subdirectories will also inherit these ACLs.</p>

<p>When this directory is exported by NFSv4, this ACL is honored by the
client.</p>

<p>For more information, see acl(5) and setfacl(1).</p>

<p>A word of warning, not all NFS servers will support this, and I’m
fairly certain you must be using NFSv4 for this to work.  I’ve tested
it with a RHEL8 nfs-server, but YMMV for other nfs servers.</p>

<h1 id="why-would-i-need-to-do-this">WHY would I need to do this?</h1>

<p>OpenShift, unique among K8s distributions, semi-randomizes the UID and
GID of it’s pods.  This is a security measure that adds barriers for
attackers.</p>

<p>When two pods need to share a PV, we need to carefully construct
permissions so that they share a common group and that group has write
permissions on all files.</p>

<p>Normally this is done by:</p>

<ol>
  <li>
    <p>Changing the group of the root of the NFS share to 0 (root) or a
defined common group</p>
  </li>
  <li>
    <p>Setting the setgid-bit, so that all new files and directories
inherit that group.</p>
  </li>
  <li>
    <p>Setting the umask to 0002, so that group read-write is set on all
files.</p>
  </li>
</ol>

<p>Step #1 and #2 above are fairly straight forward, but setting the
umask on every pod in every scenario can be difficult, especially if
you’re dealing with pre-created container images and many images (like
in a tekton pipeline)</p>

<p>Using default ACLs, instead of relying on umask, allows me to make
<em>one</em> change, on the server side, that is automatically followed by
all pods.</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="k8s" /><category term="hardware" /><category term="linux" /><category term="nfs" /><category term="openshift" /><category term="tekton" /><summary type="html"><![CDATA[I ran into an issue recently with my Tekton Pipelines with PersistentVolume Permissions.]]></summary></entry><entry><title type="html">Renaming RAID Devices</title><link href="/blog/linuxunix/2022/06/17/renaming-raid-devices.html" rel="alternate" type="text/html" title="Renaming RAID Devices" /><published>2022-06-17T00:05:00-04:00</published><updated>2022-06-17T00:05:00-04:00</updated><id>/blog/linuxunix/2022/06/17/renaming-raid-devices</id><content type="html" xml:base="/blog/linuxunix/2022/06/17/renaming-raid-devices.html"><![CDATA[<p>I learned recently that software RAID devices under Linux can have
friendly names.  I had never specified a name when creating arrays.</p>

<p>I’ve been recently updating my fileserver to RHEL 8 and working with
the original array I created in 2015 and documented in my post: <a href="/blog/linuxunix/2015/09/25/raid-6-and-lvm.html">RAID
6 and LVM</a>.  In the original
version of that post, I did not specify a name.</p>

<p>I’ve found that naming my RAID volumes has a great benefit when
reinstalling the system via Kickstart, which I’ll write about more in
detail later.</p>

<p>So, my RAID volumes are unnamed….how do I give them a name?</p>

<p>First, let’s look at our RAID volume using the <code class="language-plaintext highlighter-rouge">mdadm --detail</code>
command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># mdadm --detail /dev/md200
/dev/md200:
           Version : 1.2
     Creation Time : Thu Sep 24 17:47:54 2015
        Raid Level : raid6
        Array Size : 3906521088 (3.64 TiB 4.00 TB)
     Used Dev Size : 976630272 (931.39 GiB 1000.07 GB)
      Raid Devices : 6
     Total Devices : 6
       Persistence : Superblock is persistent

     Intent Bitmap : Internal

       Update Time : Thu Jun  9 03:11:45 2022
             State : clean 
    Active Devices : 6
   Working Devices : 6
    Failed Devices : 0
     Spare Devices : 0

            Layout : left-symmetric
        Chunk Size : 512K

Consistency Policy : bitmap

              Name : 200
              UUID : 6d90c19a:9812e659:6854747b:a89f1280
            Events : 34710

    Number   Major   Minor   RaidDevice State
       0       8       97        0      active sync   /dev/sdg1
       1       8       17        1      active sync   /dev/sdb1
       2       8       49        2      active sync   /dev/sdd1
       3       8      113        3      active sync   /dev/sdh1
       4       8       65        4      active sync   /dev/sde1
       5       8       81        5      active sync   /dev/sdf1
</code></pre></div></div>

<p>Above, there is a name field, but it’s not really a human name, just
<code class="language-plaintext highlighter-rouge">200</code>.  This is really just the <code class="language-plaintext highlighter-rouge">/dev/md###</code> number.</p>

<p>Renaming is a relatively simple operation, but it can’t be done live.</p>

<p>In my setup, I have a LVM Volume Group called <code class="language-plaintext highlighter-rouge">galadriel</code>.  I want to
rename my RAID device to also be <code class="language-plaintext highlighter-rouge">galadriel</code> to match.</p>

<ol>
  <li>
    <p>Run <code class="language-plaintext highlighter-rouge">mdadm --detail</code> and save the output somewhere for reference.</p>
  </li>
  <li>
    <p>Unmount all volumes related to either the RAID device or volume group using the RAID.</p>
  </li>
  <li>
    <p>Stop the volume group.  <code class="language-plaintext highlighter-rouge">-a n</code> deactivates the volume group.</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> vgchange -a n galadriel
</code></pre></div>    </div>
  </li>
  <li>
    <p>Stop the md device using <code class="language-plaintext highlighter-rouge">mdadm</code></p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> sudo mdadm --stop /dev/md200
</code></pre></div>    </div>
  </li>
  <li>
    <p>Reassemble with new name.  This reassembles the drive with the
<code class="language-plaintext highlighter-rouge">--name</code>.  The device names can be gleaned from the output of `mdadm
–detail.</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> sudo mdadm --assemble --update=name --name=galadriel /dev/md200 /dev/sdg1 /dev/sdb1 /dev/sdd1 /dev/sdh1 /dev/sde1 /dev/sdf1
</code></pre></div>    </div>
  </li>
  <li>
    <p>Reactivate the volume group</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> vgchange -a y galadriel
</code></pre></div>    </div>
  </li>
  <li>
    <p>Check that everthing is OK, by looking at <code class="language-plaintext highlighter-rouge">/dev/mdstat</code>, comparing new output of <code class="language-plaintext highlighter-rouge">mdadm --detail</code>, <code class="language-plaintext highlighter-rouge">vgs</code>, and <code class="language-plaintext highlighter-rouge">lvs</code>.</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # cat /proc/mdstat
 md200 : active raid6 sdf1[1] sdh1[2] sdd1[3] sda1[4] sdc1[0] sdb1[5]
       3906521088 blocks super 1.2 level 6, 512k chunk, algorithm 2 [6/6] [UUUUUU]
       bitmap: 0/8 pages [0KB], 65536KB chunk
</code></pre></div>    </div>
  </li>
  <li>
    <p>It may be required to update <code class="language-plaintext highlighter-rouge">/etc/mdadm.conf</code>.  In RHEL7+, the
UUID is used, so only the dev path may need to be updated:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cat /etc/mdadm.conf
    
   ### Snippet of relevant line:
ARRAY /dev/md/galadriel level=raid6 num-devices=6 UUID=6d90c19a:9812e659:6854747b:a89f1280
</code></pre></div>    </div>
  </li>
</ol>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="linuxunix" /><category term="linux" /><category term="x86" /><category term="raid" /><category term="lvm" /><category term="rhel" /><summary type="html"><![CDATA[I learned recently that software RAID devices under Linux can have friendly names. I had never specified a name when creating arrays.]]></summary></entry><entry><title type="html">Fixing Fedora’s default FreeIPA config</title><link href="/blog/linuxunix/2022/06/06/enterprise-login-in-fedora.html" rel="alternate" type="text/html" title="Fixing Fedora’s default FreeIPA config" /><published>2022-06-06T14:05:00-04:00</published><updated>2022-06-06T14:05:00-04:00</updated><id>/blog/linuxunix/2022/06/06/enterprise-login-in-fedora</id><content type="html" xml:base="/blog/linuxunix/2022/06/06/enterprise-login-in-fedora.html"><![CDATA[<p>I periodically re-install Fedora on my laptops.  Now that I have a
fairly stable FreeIPA setup, I’ve been joining my laptops to FreeIPA
during installation:</p>

<p>This works great for the first user, i.e. the one that is specified in
the installer.  However, other users are not able to log in after
installation.</p>

<p>To make reading easier, I’ve put <code class="language-plaintext highlighter-rouge">firstuser</code> and <code class="language-plaintext highlighter-rouge">seconduser</code> as my
two users.  I’ve also clipped out hostnames and timestamps from
<code class="language-plaintext highlighter-rouge">journalctl</code> output.</p>

<p>If attempting to log in as one of these other users via <code class="language-plaintext highlighter-rouge">ssh</code>, the
following messages are logged in the journal:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sshd[928089]: pam_sss(sshd:auth): authentication success; logname= uid=0 euid=0 tty=ssh ruser= rhost=172.31.0.50 user=seconduser
sshd[928089]: pam_sss(sshd:account): Access denied for user seconduser: 6 (Permission denied)
sshd[928089]: Failed password for seconduser from 172.31.0.50 port 42840 ssh2
sshd[928089]: fatal: Access denied for user seconduser by PAM account configuration [preauth]
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Failed password for seconduser</code> makes this seem like a password issue, but
it’s not.  An actual bad password looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>krb5_child[979500]: Preauthentication failed
krb5_child[979500]: Preauthentication failed
sshd[979494]: pam_sss(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=172.31.0.50 user=seconduser
sshd[979494]: pam_sss(sshd:auth): received for user seconduser: 7 (Authentication failure)
sshd[979494]: Failed password for seconduser from 172.31.0.50 port 39754 ssh2
</code></pre></div></div>

<p>The real error, is actually coming from PAM and we can see the
difference from my scenario vs the true wrong password.  If we look at
the first example, the real error is:</p>

<p><code class="language-plaintext highlighter-rouge">pam_sss(sshd:account): Access denied for user seconduser: 6 (Permission denied)</code></p>

<p>compare to the wrong password scenario, where the error is this:</p>

<p><code class="language-plaintext highlighter-rouge">pam_sss(sshd:auth): received for user seconduser: 7 (Authentication failure)</code></p>

<p>From this, we can see that a true wrong password fails in the <code class="language-plaintext highlighter-rouge">auth</code>
modules of PAM (as is expected), but my error happens in the <code class="language-plaintext highlighter-rouge">account</code>
modules.</p>

<p>So, one of the PAM <code class="language-plaintext highlighter-rouge">account</code> modules is denying my user.</p>

<p>Looking at <code class="language-plaintext highlighter-rouge">/etc/pam.d/password-auth</code>, which is a symlink to <code class="language-plaintext highlighter-rouge">/etc/authselect/password-auth</code>, we see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>account     required                                     pam_unix.so
account     sufficient                                   pam_localuser.so
account     sufficient                                   pam_usertype.so issystem
account     [default=bad success=ok user_unknown=ignore] pam_sss.so
account     required                                     pam_permit.so
</code></pre></div></div>

<p>Don’t really see anything that would allow one user, but deny an other.</p>

<p><code class="language-plaintext highlighter-rouge">pam_sss.so</code> is the module that interfaces with FreeIPA via <code class="language-plaintext highlighter-rouge">sssd</code>.
It sources it’s configuration in <code class="language-plaintext highlighter-rouge">/etc/sssd/sssd.conf</code>, and there we
find our culprit:</p>

<p>From <code class="language-plaintext highlighter-rouge">sssd.conf</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[domain/mydomain.example.com]
simple_allow_users = $, firstuser
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sssd-simple(5)</code>, provides very simple user and group allow/deny
lists.  In this case, if a user is not listed in <code class="language-plaintext highlighter-rouge">simple_allow_users</code>,
then they are not allowed to login.</p>

<p>What this means is, only <code class="language-plaintext highlighter-rouge">firstuser</code> is allowed to log in.  Adding
<code class="language-plaintext highlighter-rouge">seconduser</code> to the list will allow them to log in, and so forth.</p>

<p>This seems like a bug.  At the least it’s very unexpected behavior!</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="linuxunix" /><category term="linux" /><category term="x86" /><category term="fedora" /><category term="freeipa" /><category term="ldap" /><summary type="html"><![CDATA[I periodically re-install Fedora on my laptops. Now that I have a fairly stable FreeIPA setup, I’ve been joining my laptops to FreeIPA during installation:]]></summary></entry><entry><title type="html">OpenShift authentication with IPA</title><link href="/blog/linuxunix/2022/05/11/ipa-openshift-ldap.html" rel="alternate" type="text/html" title="OpenShift authentication with IPA" /><published>2022-05-11T14:05:00-04:00</published><updated>2022-05-11T14:05:00-04:00</updated><id>/blog/linuxunix/2022/05/11/ipa-openshift-ldap</id><content type="html" xml:base="/blog/linuxunix/2022/05/11/ipa-openshift-ldap.html"><![CDATA[<h1 id="openshift-and-ipa-series">OpenShift and IPA Series</h1>

<p>This post is part of a collection of blog posts related to OpenShift
and FreeIPA (aka idM).</p>

<ul>
  <li><a href="/blog/linuxunix/2020/11/04/wildcard-ipa-openshift.html">OpenShift Certificate from IPA on RHEL 8</a></li>
  <li><a href="/blog/linuxunix/2022/05/11/ipa-openshift-ldap.html">OpenShift authentication with IPA</a></li>
  <li>OpenShift Group Syncing with IPA (Not yet published)</li>
  <li>Automated Certificate Management with IPA and cert-manager (Not yet published)</li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>In the previous post, <a href="/blog/linuxunix/2020/11/04/wildcard-ipa-openshift.html">OpenShift Certificate from IPA on RHEL 8</a>, I explained how to
create certificates for an OpenShift cluster using FreeIPA/RHEL idM.</p>

<p>Now, I want to be able to log into my OpenShift cluster using my
FreeIPA credentials.</p>

<h1 id="openshift-background">OpenShift Background</h1>

<p>OpenShift has the concept of an <code class="language-plaintext highlighter-rouge">IdentityProvider</code> which connects an
source of identity verification, like FreeIPA’s LDAP server, to
OpenShift.</p>

<p>OpenShift 4.x uses an object called an <code class="language-plaintext highlighter-rouge">OAuth</code> to configure identity
providers like LDAP.  For OpenShift 3.x, this configuration is
similar, but locaed in <code class="language-plaintext highlighter-rouge">/etc/origin/master/master-config.yaml</code></p>

<p>Let’s look at an example OAuth manifest:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: config.openshift.io/v1
kind: OAuth
metadata:
  name: cluster
spec:
  identityProviders:
  - ldap:
      attributes:
        email:
        - mail
        id:
        - dn
        name:
        - cn
        preferredUsername:
        - uid
      bindDN: uid=openshift,cn=sysaccounts,cn=etc,dc=private,dc=opequon,dc=net
      bindPassword:
        name: ldap-secret 
      ca:
        name: opequon-custom-ca-oauth
      insecure: false
      url: ldaps://ipa.private.opequon.net/cn=users,cn=accounts,dc=private,dc=opequon,dc=net?uid
    mappingMethod: claim
    name: ldapidp
    type: LDAP
</code></pre></div></div>

<p>This is an example of a LDAP Identity provider, there are other
flavors if desired.  Notice <code class="language-plaintext highlighter-rouge">OAuth.spec.identityProviders</code> looks for
an array so it is possible to specify multiple providers.</p>

<p>We have a few things that need filled out here:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">name</code></li>
  <li><code class="language-plaintext highlighter-rouge">bindDN</code></li>
  <li><code class="language-plaintext highlighter-rouge">bindPassword</code></li>
  <li><code class="language-plaintext highlighter-rouge">ca</code></li>
  <li><code class="language-plaintext highlighter-rouge">url</code></li>
  <li><code class="language-plaintext highlighter-rouge">attributes</code></li>
  <li><code class="language-plaintext highlighter-rouge">mappingMethod</code></li>
</ul>

<h2 id="name">Name</h2>

<p>This is the display name for this <code class="language-plaintext highlighter-rouge">IdentityProvider</code>.  When logging
on, if multiple <code class="language-plaintext highlighter-rouge">IdentityProviders</code> are configured, the user will see
this name to choose from.  In the case of only having a single
provider, this is almost never seen.</p>

<p>Whatever value for <code class="language-plaintext highlighter-rouge">name</code> is, it should be something meaningful to the
humans that interact with this OpenShift cluster.</p>

<h2 id="binddn-and-bindpassword">bindDN and bindPassword</h2>

<p>The username and password for accessing the cluster.  <code class="language-plaintext highlighter-rouge">bindPassword</code>
is normally sourced from a Kubernetes <code class="language-plaintext highlighter-rouge">Secret</code>.</p>

<h2 id="ca">ca</h2>

<p>The certificate chain for the LDAP server.  Almost every LDAP server
uses a certificate signed by a non-public certificate authority
(i.e. not included in the default RHEL certificate authority bundle),
therefore this is almost always required.</p>

<p>Since FreeIPA acts as a certificate authority, this will need to be
specified in the final configuration.</p>

<p>This is normally sourced from a <code class="language-plaintext highlighter-rouge">ConfigMap</code>.</p>

<h2 id="url">url</h2>

<p>This is the URL of the LDAP server with a query string, as defined by
<a href="https://datatracker.ietf.org/doc/html/rfc4516">RFC4516</a> (which
obsoletes, <a href="https://datatracker.ietf.org/doc/html/rfc2254">RFC2254</a>).
The query string is, along with the <code class="language-plaintext highlighter-rouge">attributes</code>, the most important
part fo the configuration.  If it is wrong, then no one can log in,
and it is significantly easier to get wrong than the other fields.</p>

<p>The RFCs are rather dense reading, so it’s worthwhile to break down the
parts of a query string:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>schema://hostname:port/base_dn?attributes?scope?filter
                       ^                             ^
                       +-----------------------------+
                              Query String
</code></pre></div></div>

<p>After the schema, hostname, and port, there are four fields:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">base_dn</code></li>
  <li><code class="language-plaintext highlighter-rouge">attributes</code></li>
  <li><code class="language-plaintext highlighter-rouge">scope</code></li>
  <li><code class="language-plaintext highlighter-rouge">filter</code></li>
</ul>

<p>LDAP organizes it’s data into a tree-like structure.  <code class="language-plaintext highlighter-rouge">base_dn</code>
specifies where in the tree to start searching. This can be visually
seen by tools like <a href="https://directory.apache.org/studio/">Apache Directory
Studio</a>.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/ipa/ads_ss.png" alt="Apache Directory Studio Screenshot" />
  <figcaption>Apache Directory Studio Screenshot</figcaption>
</figure>
</div>

<p><code class="language-plaintext highlighter-rouge">attributes</code> specify what attributes from the found entities to return
from the search.  An entity is a node on the tree that represents
something, like a user account or a group.  These, of course, can have
many attributes.  Some common examples are <code class="language-plaintext highlighter-rouge">mail</code> for e-mail address,
<code class="language-plaintext highlighter-rouge">sn</code> for surname, <code class="language-plaintext highlighter-rouge">givenName</code>, <code class="language-plaintext highlighter-rouge">loginShell</code>, <code class="language-plaintext highlighter-rouge">homeDirectory</code>.</p>

<p>For OpenShift, this should be a attribute that is going to be unique
to each entity in the entities returned by the query, like <code class="language-plaintext highlighter-rouge">uid</code> which
represents a Linux username.</p>

<p>Again, a tool like <a href="https://directory.apache.org/studio/">Apache Directory
Studio</a> can be very helpful
exploring the LDAP structure.</p>

<p><code class="language-plaintext highlighter-rouge">scope</code> determines if or how the search recurses through the tree.  The options are:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">base</code></li>
  <li><code class="language-plaintext highlighter-rouge">one</code></li>
  <li><code class="language-plaintext highlighter-rouge">sub</code></li>
</ul>

<p>The default is <code class="language-plaintext highlighter-rouge">sub</code></p>

<p><code class="language-plaintext highlighter-rouge">base</code> restricts itself to ONLY the <code class="language-plaintext highlighter-rouge">base_dn</code> and is not really useful in this situation.</p>

<p><code class="language-plaintext highlighter-rouge">one</code> searches only the first level below the <code class="language-plaintext highlighter-rouge">base_dn</code>.</p>

<p><code class="language-plaintext highlighter-rouge">sub</code> fully recurses the tree below the <code class="language-plaintext highlighter-rouge">base_dn</code>.</p>

<p>The <a href="https://ldapwiki.com">LDAP Wiki</a> has a good page explaining <a href="https://ldapwiki.com/wiki/LDAP%20Search%20Scopes">LDAP
Search Scopes</a>.</p>

<p>Finally, <code class="language-plaintext highlighter-rouge">filter</code> is an LDAP Filter that allows refinement of the
resultset even further.  This is a really deep topic, but a simple
example of this is <code class="language-plaintext highlighter-rouge">(objectclass=person)</code>.  This filter will only
return entities that are of <code class="language-plaintext highlighter-rouge">objectclass</code> <code class="language-plaintext highlighter-rouge">person</code>.</p>

<p>If no <code class="language-plaintext highlighter-rouge">filter</code> is specified, then <code class="language-plaintext highlighter-rouge">(objectclass=*)</code> is the default.
This filter is true for all entities.</p>

<p>The <a href="https://ldapwiki.com">LDAP Wiki</a> has a lot of good examples of
<a href="https://ldapwiki.com/wiki/LDAP%20Query%20Examples">LDAP Queries</a>.</p>

<p>With this information, we can create a URL for IPA.  Fortunately, for
the default install of FreeIPA, we can have a fairly simple query
string.</p>

<p>From the example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ldaps://ipa.private.opequon.net/cn=users,cn=accounts,dc=private,dc=opequon,dc=net?uid
</code></pre></div></div>

<p>This example omits some parameters.  If we were to fill in the default explicitly, it would look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ldaps://ipa.private.opequon.net/cn=users,cn=accounts,dc=private,dc=opequon,dc=net?uid?sub?(objectclass=*)
</code></pre></div></div>

<p>In FreeIPA, all users are always listed under
<code class="language-plaintext highlighter-rouge">cn=users,cn=accounts,dc=mydomain</code>.  This differs some other LDAP
systems (specifically Active Directory).  This makes our search really
simple, as we can just pull every entity out of this <code class="language-plaintext highlighter-rouge">base_dn</code> and be
assured that all users will be able to log into OpenShift.</p>

<h3 id="an-aside--restricting-access">An Aside:  Restricting Access</h3>

<p>In this example, the LDAP URL is constructed so that all users can log
in to OpenShift.</p>

<p>In many situations, there is a desire to lock this down to allow, for
example, only users from a certain group to log in or other similar
restriction.</p>

<p>While this is certainly possible, with the correct LDAP filter.  I
would posit that this is an anti-pattern, and that access control is
much better co-ordinated via Groups and RBAC policies.</p>

<p>I have several reasons for this:</p>

<ol>
  <li>
    <p>It’s imperative that the LDAP query returns quickly.  This query
is run every time a user logs in.  If the query takes a long time to
run, then the user experience will suffer.</p>
  </li>
  <li>
    <p>Changing the <code class="language-plaintext highlighter-rouge">OAuth</code> object can potentially cause loss of service,
especially if updated with incorrect values.</p>
  </li>
</ol>

<p>If you configure a very complicated query (especially if using nested
group search on a complicated LDAP tree), then #1 is very possible.</p>

<p>And, secondly, if you specify a complicated query it increases the
chances of having to change the query in the future, which increases
the changes for mistakes when updating the <code class="language-plaintext highlighter-rouge">OAuth</code> object.</p>

<p>Therefore, my opinion is to let everyone log in.  It’s a simple query
and is unlikely to every change, unless there are massive change to
the LDAP structure.</p>

<p>In that situation, controlling access is done via <code class="language-plaintext highlighter-rouge">Groups</code> and
Role-Based Access Control.  This is easier to implement, less risky to
change, and much more flexible.  A future blog post will cover this
topic in detaill.</p>

<h2 id="attributes">attributes</h2>

<p>When a user logs into OpenShift using an <code class="language-plaintext highlighter-rouge">IdentityProvider</code>, a set of
proxy Kubernetes object is created to represent that user.  These
objects are used predominately in role based access control and group
membership, as well as in creation metadata for certain other objects.</p>

<p>The <code class="language-plaintext highlighter-rouge">attributes</code> field configures the <code class="language-plaintext highlighter-rouge">IdentityProvider</code> to map
attributes in the LDAP entity to fields in these proxy objects.</p>

<p>There are four attributes, let’s go over them quickly:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">name</code></li>
  <li><code class="language-plaintext highlighter-rouge">email</code></li>
  <li><code class="language-plaintext highlighter-rouge">id</code></li>
  <li><code class="language-plaintext highlighter-rouge">preferredUsername</code></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">name</code> is a human readable name of the user.  In Free IPA, this is in
the <code class="language-plaintext highlighter-rouge">cn</code> attribute of the LDAP entity.</p>

<p><code class="language-plaintext highlighter-rouge">email</code> is, obviously, the user’s email address.  In Free IPA, this is in
the <code class="language-plaintext highlighter-rouge">mail</code> attribute of the LDAP entity.</p>

<p><code class="language-plaintext highlighter-rouge">id</code> is the <em>most important</em> of these attribute, as it is meant to map
to a LDAP field that uniquely identifies the user.  As a consequence,
this attribute should never change during the life of the LDAP entity.
If it does change then the link between the user entity in LDAP and
the <code class="language-plaintext highlighter-rouge">User</code> object in OpenShift will be broken, and the user will no
longer be able to log in.</p>

<p>In Free IPA, the <code class="language-plaintext highlighter-rouge">id</code> attribute should be mapped to the <code class="language-plaintext highlighter-rouge">dn</code> attribute
of the LDAP entity.</p>

<p><code class="language-plaintext highlighter-rouge">preferredUsername</code> is optional, but useful. By default, the
<code class="language-plaintext highlighter-rouge">IdentityProvider</code> will use <code class="language-plaintext highlighter-rouge">id</code> as the username for that user in
OpenShift.  Unfortunately, <code class="language-plaintext highlighter-rouge">dn</code> is a big long, ugly LDAP Distinguished
Name, like this:
<code class="language-plaintext highlighter-rouge">uid=cfh,cn=users,cn=accounts,dc=private,dc=opequon,dc=net</code>.</p>

<p><code class="language-plaintext highlighter-rouge">preferredUsername</code>, if specified, causes a different attribute to be
used for username.  Generally, I want to use the same username as I
use for logging into Linux systems.  In Free IPA, this is in the <code class="language-plaintext highlighter-rouge">uid</code>
attribute of the LDAP entity.</p>

<h2 id="mappingmethod">mappingMethod</h2>

<p>When multiple <code class="language-plaintext highlighter-rouge">IdentityProviders</code> are configured in a single cluster,
then the <code class="language-plaintext highlighter-rouge">mappingMethod</code> is set to determine how username conflicts
are handled.</p>

<p>In the case where you only have one <code class="language-plaintext highlighter-rouge">IdentityProvider</code> configured,
then <code class="language-plaintext highlighter-rouge">claim</code> is the right value.</p>

<h1 id="putting-it-all-together">Putting it all together</h1>

<h2 id="pre-requisites">Pre-requisites</h2>

<h3 id="service-account">Service Account</h3>

<p>I need a read-only service account in my FreeIPA LDAP.  This is not a
regular user.  I don’t want it to be able to log into any hosts or
even the IPA console.  I <em>only</em> want it to be able to do queries
against FreeIPA’s LDAP.</p>

<p>To do this, create a <a href="https://www.freeipa.org/page/HowTo/LDAP#System_Accounts">LDAP System
Account</a>.</p>

<p>All these examples use the base domain of my system
(<code class="language-plaintext highlighter-rouge">dc=private,dc=opequon,dc=net</code>).  You’ll obviously need to swap this
out with the specifics of your FreeIPA installation.</p>

<p>On the FreeIPA server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@ipa ~]# ldapmodify -x -D 'cn=Directory Manager' -W
Enter LDAP Password: 
# Paste in the below
dn: uid=openshift,cn=sysaccounts,cn=etc,dc=private,dc=opequon,dc=net  &lt;- CHANGE OPENSHIFT
changetype: add                                                          TO DESIRED NAME OF
objectclass: account                                                     SERVICE ACCOUNT
objectclass: simplesecurityobject
uid: openshift          &lt;-  THIS SHOULD MATCH THE DN ABOVE
userPassword: changeMe  &lt;-  THIS IS YOUR SERVICE ACCOUNT PASSWORD
passwordExpirationTime: 20380119031407Z &lt;- 2038 EFFECTIVELY THE END OF TIME
nsIdleTimeout: 0

^D  &lt;- ACTUALLY TYPE Control-D
</code></pre></div></div>

<p>If successful, you should see the following output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adding new entry "uid=openshift,cn=sysaccounts,cn=etc,dc=private,dc=opequon,dc=net"
</code></pre></div></div>

<p>You can then test this service account using a command similar to the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ldapsearch  -x -D 'uid=openshift,cn=sysaccounts,cn=etc,dc=private,dc=opequon,dc=net' -W
</code></pre></div></div>

<p>This will spit out every object in the LDAP database.  Of course you
can filter, see <code class="language-plaintext highlighter-rouge">ldapsearch(1)</code>.</p>

<p>For both the <code class="language-plaintext highlighter-rouge">ldapmodify</code> and <code class="language-plaintext highlighter-rouge">ldapsearch</code> command, the arguments mean the following:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-x</code> - use simple authentication (instead of a Kerberos ticket)</li>
  <li><code class="language-plaintext highlighter-rouge">-D</code> - the distinguished name of the user</li>
  <li><code class="language-plaintext highlighter-rouge">-W</code> - prompt for password</li>
</ul>

<h3 id="certificate-authority">Certificate Authority</h3>

<p>This file is normally found at <code class="language-plaintext highlighter-rouge">/etc/ipa/ca.crt</code> on any machine
connected to FreeIPA.</p>

<p>Grab this file, so it can put it into a config map later, or
alernatively run <code class="language-plaintext highlighter-rouge">oc</code> commands from a Linux machine that is connected
to FreeIPA.</p>

<h2 id="openshift-configuration">OpenShift Configuration</h2>

<h3 id="creating-secrets-and-configmaps">Creating Secrets and ConfigMaps</h3>

<p>The <code class="language-plaintext highlighter-rouge">OAuth</code> object we are creating requires a <code class="language-plaintext highlighter-rouge">Secret</code> for the LDAP
service account password.</p>

<p>This secret must be in the <code class="language-plaintext highlighter-rouge">openshift-config</code> <code class="language-plaintext highlighter-rouge">Project</code>, but can be
named anything so long as that name is used in the later <code class="language-plaintext highlighter-rouge">OAuth</code>
object.</p>

<p>To do so via the command line:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># oc project openshift-config 
# oc create secret generic ldap-secret --from-literal=bindPassword=changeMe

</code></pre></div></div>

<p>This will create a <code class="language-plaintext highlighter-rouge">Secret</code> like the below.  Alternatively, this
manifest can be applied directly using <code class="language-plaintext highlighter-rouge">oc create</code>, just be sure to
substitute the value of <code class="language-plaintext highlighter-rouge">data.bindPassword</code> with the Base64 encoded
string of your actual password.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: v1
kind: Secret
metadata:
  name: ldap-secret
  namespace: openshift-config
type: Opaque
data:
  bindPassword: Y2hhbmdlTWU=
</code></pre></div></div>

<p>In addition to the <code class="language-plaintext highlighter-rouge">Secret</code>, the FreeIPA certificate authority chain
must be in a <code class="language-plaintext highlighter-rouge">ConfigMap</code> object in the <code class="language-plaintext highlighter-rouge">openshift-config</code> namespace.
This file is normally found at <code class="language-plaintext highlighter-rouge">/etc/ipa/ca.crt</code> on any machine
connected to FreeIPA.</p>

<p>To create this <code class="language-plaintext highlighter-rouge">ConfigMap</code> via the command line:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oc project
oc create cm custom-ca-oauth --from-file=ca.crt=/etc/ipa/ca.crt -n openshift-config
</code></pre></div></div>

<p>Alternatively, this manifest can be applied directly using <code class="language-plaintext highlighter-rouge">oc
create</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-ca-oauth
  namespace: openshift-config
data:
  ca.crt: |
    -----BEGIN CERTIFICATE-----
    REPLACE ME
    -----END CERTIFICATE-----
</code></pre></div></div>

<h3 id="creating-oauth-object">Creating OAuth Object</h3>

<p>With the LDAP service account created and the <code class="language-plaintext highlighter-rouge">Secrets</code> and
<code class="language-plaintext highlighter-rouge">ConfigMaps</code> in place, it’s time to create to update the OAuth object.</p>

<p>In OpenShift 4, an OAuth object will exist by default regardless of if
there are any <code class="language-plaintext highlighter-rouge">IdentityProviders</code> configured.  We could use <code class="language-plaintext highlighter-rouge">oc
replace</code> to overwrite this object or just edit the object using either
<code class="language-plaintext highlighter-rouge">oc edit</code> or the web console.</p>

<p>Regardless of method chosen, the resulting object definition should
look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: config.openshift.io/v1
kind: OAuth
metadata:
  name: cluster
spec:
  identityProviders:
  - ldap:
      attributes:
        email:
        - mail
        id:
        - dn
        name:
        - cn
        preferredUsername:
        - uid
      bindDN: uid=openshift,cn=sysaccounts,cn=etc,dc=private,dc=opequon,dc=net
      bindPassword:
        name: ldap-secret       # MUST MATCH OUR SECRET FOR LDAP
      ca:
        name: custom-ca-oauth   # MUST MATCH OUR CA CONFIGMAP
      insecure: false
      url: ldaps://ipa.private.opequon.net/cn=users,cn=accounts,dc=private,dc=opequon,dc=net?uid
    mappingMethod: claim
    name: ldapidp
    type: LDAP
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">bindDN</code> and <code class="language-plaintext highlighter-rouge">url</code> must, of course, be updated to match your
environment.  The names of the <code class="language-plaintext highlighter-rouge">Secret</code> and Certificate Authority
<code class="language-plaintext highlighter-rouge">ConfigMap</code> must match the names of those created in the previous
section.</p>

<p>Once this is applied, the <code class="language-plaintext highlighter-rouge">authentication</code> <code class="language-plaintext highlighter-rouge">ClusterOperator</code> will
apply the configuration you can check the status by looking at the
<code class="language-plaintext highlighter-rouge">ClusterOperator</code> objects (short name for these is <code class="language-plaintext highlighter-rouge">co</code>)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># oc get co authentication
NAME             VERSION   AVAILABLE   PROGRESSING   DEGRADED   SINCE   MESSAGE
authentication   4.10.12   True        False         False      24h     
</code></pre></div></div>

<p>While being configured, the <code class="language-plaintext highlighter-rouge">PROGRESSING</code> column will flip to false.  This configuration should only take a few minutes to apply.</p>

<p>If it the <code class="language-plaintext highlighter-rouge">DEGRADED</code> column flips to true for a long period (or if
<code class="language-plaintext highlighter-rouge">AVAILABLE</code> becomes false), then use <code class="language-plaintext highlighter-rouge">oc describe co authentication</code>
to see events related to the problem.</p>

<h3 id="testing-logging-in">Testing Logging in</h3>

<p>After the <code class="language-plaintext highlighter-rouge">authentication</code> <code class="language-plaintext highlighter-rouge">ClusterOperator</code> applies the
configuration, on next log in attempt, the user will be presented with
an option to log in using FreeIPA as the provider.  The name of this
provider is specified in the <code class="language-plaintext highlighter-rouge">OAuth</code> object
<code class="language-plaintext highlighter-rouge">spec.identityprovider.ldap.name</code> field.  In our example, this is
<code class="language-plaintext highlighter-rouge">ldapipa</code>.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/ipa/login.png" alt="OpenShift Login Screen" />
  <figcaption>OpenShift Login Screen</figcaption>
</figure>
</div>

<p>Once logged in, we should be able to see our <code class="language-plaintext highlighter-rouge">User</code> object.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># oc get users
NAME   UID                                    FULL NAME    IDENTITIES
cfh    826d995a-2045-42dd-a4fa-81d338ebbefe   Clark Hale   ldapidp:dWlkPWNmaCxjbj11c2Vycyxjbj1hY2NvdW50cyxkYz1wcml2YXRlLGRjPW9wZXF1b24sZGM9bmV0
</code></pre></div></div>

<p>Additionally, we can see the <code class="language-plaintext highlighter-rouge">Identity</code> object</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@meriadoc opequon_labs_ocp_playbooks]# oc get identity
NAME                                                                                   IDP NAME   IDP USER NAME                                                                  USER NAME   USER UID
ldapidp:dWlkPWNmaCxjbj11c2Vycyxjbj1hY2NvdW50cyxkYz1wcml2YXRlLGRjPW9wZXF1b24sZGM9bmV0   ldapidp    dWlkPWNmaCxjbj11c2Vycyxjbj1hY2NvdW50cyxkYz1wcml2YXRlLGRjPW9wZXF1b24sZGM9bmV0   cfh         826d995a-2045-42dd-a4fa-81d338ebbefe
</code></pre></div></div>

<p>We can see that these long strings are actually the value of the <code class="language-plaintext highlighter-rouge">id</code>
<code class="language-plaintext highlighter-rouge">attribute</code>, which we set to <code class="language-plaintext highlighter-rouge">dn</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@meriadoc opequon_labs_ocp_playbooks]# echo "dWlkPWNmaCxjbj11c2Vycyxjbj1hY2NvdW50cyxkYz1wcml2YXRlLGRjPW9wZXF1b24sZGM9bmV0" | base64 -d
uid=cfh,cn=users,cn=accounts,dc=private,dc=opequon,dc=net
</code></pre></div></div>

<h1 id="next-steps">Next Steps</h1>

<p>Now that users can log in, the next step is to synchronize groups from
FreeIPA and then assigning Roles to those users.</p>

<ul>
  <li><a href="/blog/linuxunix/2020/11/04/wildcard-ipa-openshift.html">OpenShift Certificate from IPA on RHEL 8</a></li>
  <li><a href="/blog/linuxunix/2022/05/11/ipa-openshift-ldap.html">OpenShift authentication with IPA</a></li>
  <li>OpenShift Group Syncing with IPA (Not yet published)</li>
  <li>Automated Certificate Management with IPA and cert-manager (Not yet published)</li>
</ul>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="linuxunix" /><category term="linux" /><category term="x86" /><category term="kubernetes" /><category term="openshift" /><category term="ldap" /><category term="rhel8" /><summary type="html"><![CDATA[OpenShift and IPA Series]]></summary></entry><entry><title type="html">Removing labels or annotations from K8s objects using Ansible</title><link href="/blog/k8s/2022/03/25/removing-labels-ansible-k8s.html" rel="alternate" type="text/html" title="Removing labels or annotations from K8s objects using Ansible" /><published>2022-03-25T02:42:00-04:00</published><updated>2022-03-25T02:42:00-04:00</updated><id>/blog/k8s/2022/03/25/removing-labels-ansible-k8s</id><content type="html" xml:base="/blog/k8s/2022/03/25/removing-labels-ansible-k8s.html"><![CDATA[<p>Just a small little trick worth recording.  To remove a label,
annotation, or likely any other key from a Kubernetes object using
Ansible’s <code class="language-plaintext highlighter-rouge">k8s</code> module, you must set that key to NULL.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    - name: Remove Annotation Blah
      k8s:
       api_key: ""
       state: present
       definition:
         apiVersion: v1
         kind: Node
         metadata:
           name: varda.private.opequon.net
           annotations:
             blah: NULL
</code></pre></div></div>

<p>The above snippet removes the <code class="language-plaintext highlighter-rouge">blah</code> annotation from this node object.</p>

<p>Certainly not intuitive…took me a good day or two of searching when
I need this last year.</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="k8s" /><category term="openshift" /><category term="k8s" /><category term="linux" /><category term="containers" /><category term="ansible" /><summary type="html"><![CDATA[Just a small little trick worth recording. To remove a label, annotation, or likely any other key from a Kubernetes object using Ansible’s k8s module, you must set that key to NULL.]]></summary></entry><entry><title type="html">Relative Paths in AmigaDOS Commands</title><link href="/blog/amiga/2022/03/25/relative-directories-amigaos.html" rel="alternate" type="text/html" title="Relative Paths in AmigaDOS Commands" /><published>2022-03-25T01:00:00-04:00</published><updated>2022-03-25T01:00:00-04:00</updated><id>/blog/amiga/2022/03/25/relative-directories-amigaos</id><content type="html" xml:base="/blog/amiga/2022/03/25/relative-directories-amigaos.html"><![CDATA[<p>I’ve revived my old Amiga computers and have been playing around with them.</p>

<p>The AmigaDOS is an interesting piece of history.  It was based upon
<a href="https://en.wikipedia.org/wiki/TRIPOS">TRIPOS</a>, which was initially released
in 1978.  UNIX was released initially in 1973ish, so it’s unclear to
me how much influence UNIX had on TRIPOS and subsequently AmigaDOS.</p>

<p>I have a suspicion TRIPOS was relatively free of UNIX influence, based
on the absence of <code class="language-plaintext highlighter-rouge">.</code> and <code class="language-plaintext highlighter-rouge">..</code> for relative file access.</p>

<p>For the uninitiated, <code class="language-plaintext highlighter-rouge">.</code> and <code class="language-plaintext highlighter-rouge">..</code> are special files that occur in
every UNIX directory. <code class="language-plaintext highlighter-rouge">.</code> refers to the current directory and <code class="language-plaintext highlighter-rouge">..</code>
refers to the parent directory.  The root directory, i.e. <code class="language-plaintext highlighter-rouge">/</code>, is the
only special case.  Since it has no parent, root’s <code class="language-plaintext highlighter-rouge">..</code> refers to
itself.</p>

<p>What this means is, if I have a tree like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ tree
test
├── a
│   └── file1
├── b
│   └── sub
│       ├── file2
│       └── subsub
└── c
    └── sea_file
</code></pre></div></div>

<p>I can easily refer to files in OTHER directories than my current working directory.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>### My current working directory is test.
$ pwd
/test

### To refer to a file under test I can use '.' to refer to '/path/to/test'
$ cat ./a/file1
      ^
      +------------- The full for file is /test/.
                     but it refers to /test

### If I change my working directory to b/sub
$ cd b/sub

### I can still easily refer to files in other directories:

$ cat ../../a/file1
      ^  ^
      |  +----- .. of /test/b/, which refers to /test/
      +-------- .. of /test/b/sub which refers to /test/b/

</code></pre></div></div>

<p>This concept is <em>so</em> essential in UNIX/Linux that was at a bit of a
loss when I started using the AmigaDOS shell again.</p>

<p>In UNIX, I’m very used to doing this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cp -r /path/to/some/dir/* . 
</code></pre></div></div>

<p>To copy everything in <code class="language-plaintext highlighter-rouge">/path/to/some/dir/</code> to the current directory.</p>

<p>In AmigaDOS, I reflexively ran a similar command (note: <code class="language-plaintext highlighter-rouge">#?</code> is
equivalent to <code class="language-plaintext highlighter-rouge">*</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5.Ram Disk:&gt; copy work:test/#? .
   .   [created]
   a..copied.
   b..copied.
</code></pre></div></div>

<p>This <em>created</em> a directory called <code class="language-plaintext highlighter-rouge">.</code> and copied my files into it!
NOT WHAT I WANTED!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5.Ram Disk:&gt; dir
     . (dir)
</code></pre></div></div>

<p>This leads me to wonder….how does one do relative pathing in the
Amiga Shell?  If I used to know years ago, I’ve forgotten.</p>

<p>There are basically three characters that end up providing similar
functionality to <code class="language-plaintext highlighter-rouge">.</code> and <code class="language-plaintext highlighter-rouge">..</code> in UNIX:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">:</code></li>
  <li><code class="language-plaintext highlighter-rouge">/</code></li>
  <li><code class="language-plaintext highlighter-rouge">""</code></li>
</ul>

<p>Rather than having a unified file tree, AmigaOS has volumes each with
their own name (similar to CP/M or VMS and derivative operating
systems).</p>

<p>Volumes names come from three sources:</p>

<ul>
  <li>
    <p>File system labels, e.g. <code class="language-plaintext highlighter-rouge">Workbench:</code>, the normal volume name of the
bootable AmigaOS partition.</p>
  </li>
  <li>
    <p>Device names, e.g. <code class="language-plaintext highlighter-rouge">DF0:</code> for the first floppy disk.</p>
  </li>
  <li>
    <p>Assigns which map to directories in volumes to volume names, e.g.
<code class="language-plaintext highlighter-rouge">C:</code> which normally goes to <code class="language-plaintext highlighter-rouge">SYS:C</code> and <code class="language-plaintext highlighter-rouge">SYS:</code> which maps to the
boot volume.</p>
  </li>
</ul>

<p>There’s only one name space for volumes, so multiple names could map
to the exact same storage location.  However, generally, the AmigaDOS
Shell will show the file system label.</p>

<p>For example, the first floppy disk has a device name of <code class="language-plaintext highlighter-rouge">DF0:</code>, which
never changes no matter which disk is inserted.  A disk will have a
file system label like <code class="language-plaintext highlighter-rouge">Workbench:</code> and if that disk is booted from,
<code class="language-plaintext highlighter-rouge">SYS:</code> is also assigned to the disk.  All three of these volume names
point to the same storage.</p>

<p>Additionally, Assigns do not work the same when they point to
sub directories of other volumes.  If I do this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5.Ram Disk:&gt; cd c:
5.Workbench:C&gt;
</code></pre></div></div>

<p>Notice my current working directory changes to what <code class="language-plaintext highlighter-rouge">C:</code> references,
rather than have it appear as if it were an actual volume.</p>

<p>With that in mind, let’s return to my example and assume I have a
volume name <code class="language-plaintext highlighter-rouge">test:</code> with the following tree:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>test:
├── a
│   └── file1
├── b
│   └── sub
│       ├── file2
│       └── subsub
└── c
    └── sea_file
</code></pre></div></div>

<p>So long as my current working directory is somewhere under <code class="language-plaintext highlighter-rouge">test:</code>, I
can can list the files in the root of <code class="language-plaintext highlighter-rouge">test:</code> like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5.TEST:c&gt; dir :
     a (dir)
     b (dir)
     c (dir)
</code></pre></div></div>

<p>If my current working directory is <code class="language-plaintext highlighter-rouge">test:b/sub</code>, I can copy
<code class="language-plaintext highlighter-rouge">test:c/sea_file</code> to there like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5.TEST:b/sub&gt; dir
  file2   subsub
5.TEST:b/sub&gt; copy //c/sea_file ""
5.TEST:b/sub&gt; dir
  file2     sea_file  subsub
</code></pre></div></div>

<p>In this case, <code class="language-plaintext highlighter-rouge">""</code> acts just like <code class="language-plaintext highlighter-rouge">.</code> in UNIX, and <code class="language-plaintext highlighter-rouge">//</code> acts just like
<code class="language-plaintext highlighter-rouge">../../</code>.</p>

<p>This doesn’t seem as elegant as UNIX, but it is functional.  However,
I’m sometimes unable to tell if I <em>actually</em> find UNIX design to be
good or if I’m just used to it.</p>

<p>Originally, I was going to write that I couldn’t find this information
in the AmigaOS documentation, but as I was finishing up this blog
post, I found basically my exact description <a href="https://wiki.amigaos.net/wiki/AmigaOS_Manual:_AmigaDOS_Working_With_AmigaDOS">under the Command Line
Characters</a>
section (sorry it’s not a direct link).  No mention of “relative path”
references, though…so it is a bit hard to find.</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="amiga" /><category term="amiga" /><category term="amigaos" /><category term="vintagecomputing" /><summary type="html"><![CDATA[I’ve revived my old Amiga computers and have been playing around with them.]]></summary></entry><entry><title type="html">Azure Site-to-Site VPN and home network integration</title><link href="/blog/linuxunix/2022/03/07/azure-s2s-vpn-and-dns.html" rel="alternate" type="text/html" title="Azure Site-to-Site VPN and home network integration" /><published>2022-03-07T13:30:00-05:00</published><updated>2022-03-07T13:30:00-05:00</updated><id>/blog/linuxunix/2022/03/07/azure-s2s-vpn-and-dns</id><content type="html" xml:base="/blog/linuxunix/2022/03/07/azure-s2s-vpn-and-dns.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Recently, I’ve had to work pretty extensively with various cloud
providers.</p>

<p>Many companies I’ve worked with fully integrate their cloud provider
accounts into their existing on-premise network.  For this blog, I’m
going to call this a cloud “enclave”.</p>

<p>The hosts and services in the cloud enclave are, for all intents and
purposes, equal to resources in the “on-premise” data center.  VMs in
the cloud get IP addresses that are routable on the existing,
“on-premise” networks and DNS is configured such that cloud and
“on-premise” names roll up under the same domain and are universally
resolvable.  Most cloud workloads are only accessible via the private
network.</p>

<p>There are significant advantages to this “enclave” concept.</p>

<ol>
  <li>
    <p>Since services in the cloud are not publically accessible, the
security “threat-level” is similar to private, on-premise services.</p>
  </li>
  <li>
    <p>There are not two classes of network/DNS services.  Everything is
basically on one big happy network.</p>
  </li>
  <li>
    <p>(Arguably) things are more portable, since there’s not reliance on
a cloud provider specific way to expose services.</p>
  </li>
</ol>

<p>Creating an “enclave” and bridging it to one’s “on-premise” network is
a significantly more complicated activity than most tutorials
describe.  For a home lab, this may seem like overkill, but I consider
this a useful exercise as it more closely resembles the setup I’ve
seen in many companies.</p>

<p>This blog post describes the configuring a cloud enclave with Azure.
I had three major goals with this:</p>

<p>My cloud enclave must</p>

<ol>
  <li>be routable from my existing home network.</li>
  <li>integrate with my existing DNS solution</li>
  <li>be automated so it could be created/destroyed on-demand</li>
  <li>be minimally disruptive to my existing setup.</li>
</ol>

<h1 id="existing-home-lab">Existing Home Lab</h1>

<p>First, let’s consider my “On-Premise” home network.  I run many
services that would be considered essential in a typical corporate
network:</p>

<ul>
  <li>DNS</li>
  <li>LDAP</li>
  <li>Kerberos</li>
  <li>Certificate Management</li>
</ul>

<p>These services are provided by Red Hat Identity Management (IdM),
which is a version of FreeIPA bundled with RHEL.</p>

<p>Additionally, I also have DHCP managed by Red Hat Satellite.</p>

<p>My IP address topology is relatively simple.  I have a flat /24 as my
main network, although I’ve dabbled with splitting out separate
subnets and VLANs for things like baseboard management controllers and
storage.</p>

<p>For my enclave, I only need to extend DNS and the routable network
into the cloud.  DHCP will be handled separately by the cloud, and the
other services, e.g. LDAP, and Kerberos, will become accessible by
virtue of extending DNS and the routable network.</p>

<h1 id="designing-my-ip-space">Designing my IP Space</h1>

<p>I have used 172.31.0.0/24 as my home network IP range for many years.</p>

<p>When provisioning private cloud networks, I could choose any IP range
in the RFC1918 space, so long as it doesn’t overlap with my
“on-premise” network.</p>

<p>I decided to extend my routable network to 172.31.0.0/16, which can be
divided into 8 /19 networks, each having approximately 8192 IP
addresses.</p>

<table>
  <thead>
    <tr>
      <th>Subnet</th>
      <th>Assignment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>172.31.0.0/19</td>
      <td>On-premise Networks</td>
    </tr>
    <tr>
      <td>172.31.32.0/19</td>
      <td>Unassigned</td>
    </tr>
    <tr>
      <td>172.31.64.0/19</td>
      <td>Unassigned</td>
    </tr>
    <tr>
      <td>172.31.96.0/19</td>
      <td>Unassigned</td>
    </tr>
    <tr>
      <td>172.31.128.0/19</td>
      <td>Unassigned</td>
    </tr>
    <tr>
      <td>172.31.160.0/19</td>
      <td>Unassigned</td>
    </tr>
    <tr>
      <td>172.31.192.0/19</td>
      <td>AWS</td>
    </tr>
    <tr>
      <td>172.31.224.0/19</td>
      <td>Azure</td>
    </tr>
  </tbody>
</table>

<p>With this layout, my “on-premise” network can remain the same as it’s
always been, since 172.31.0.0/24 is a subnet of 172.31.0.0/19.
Additionally, I’ll have the potential to use other subnets of
172.31.0.0/19 in case I ever choose to deviate from a flat network.</p>

<p>With a /19, it’s highly unlikely that I’ll use all of the IP addresses
in a given allocation.  In my situation, this is ideal, since I will
never have to worry about running out of IP addresses.  In an
“Enterprise” situation, I would be more granular in specifiying things
(IPv6 can’t come soon enough!).</p>

<h1 id="designing-dns">Designing DNS</h1>

<p>Until now, I’ve had one flat DNS zone for all hostnames in my
network:  private.opequon.net.</p>

<p>I could continue with one flat zone, but through my experimentation,
this seems to be more trouble than it is worth as every cloud host
will have to register its hostname with my FreeIPA/IdM server.</p>

<p>Instead, it seems worthwhile to leverage each cloud providers internal
DNS and delegate a zone to each cloud provider.  This way a VM
started in Azure can auto register its hostname, without any on-going
effort on my part.</p>

<p>I can then bridge together the zones of my existing “on-premise”
network and each cloud zone using DNS forwarders.  My existing
“on-premise” zone can remain untouched.</p>

<p>What I’ve come up with for my network is:</p>

<table>
  <thead>
    <tr>
      <th>Zone</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>private.opequon.net</td>
      <td>“On premise” Hosts</td>
    </tr>
    <tr>
      <td>aws.private.opequon.net</td>
      <td>AWS Hosts</td>
    </tr>
    <tr>
      <td>azure.private.opequon.net</td>
      <td>Azure Hosts</td>
    </tr>
  </tbody>
</table>

<p>For Azure, there is a problem with this set up and reverse DNS, but
I’ll address that in the “Problems and Improvements” section.</p>

<h1 id="critical-components">Critical Components</h1>

<p>With my DNS and IP ranges set, we can move on to the critical
components of this “enclave”.  While there are many different “bits”
that need to be configured, the two critical components that make this
work are</p>

<ol>
  <li>Site-to-Site VPN</li>
  <li>DNS Fowarding</li>
</ol>

<h2 id="site-to-site-vpn-w-libreswan">Site-to-Site VPN w/ Libreswan</h2>

<p>Azure offers a Site-to-Site VPN object that uses IPSec tunnels to
create a VPN.</p>

<p>For the “on-premise” side of the tunnel, I use the Libreswan
implementation that’s bundled with RHEL.</p>

<p>Visually, my configuration looks like this:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/azure_site2site/Azure_DNS_Forwarding.drawio.png" alt="DNS Overview." />
  <figcaption>DNS Overview.</figcaption>
</figure>
</div>

<p>Azure does not directly support Libreswan, but does offer an Openswan
option.  Libreswan is a fork of Openswan and their configuration
format has diverged somewhat, but it’s a good starting point.</p>

<p>For reference on configuring my RHEL gateway host, <a href="https://reinhardt.dev/posts/azure-vpn-using-libreswan/">Using LibreSwan
with Azure VPN
Gateway</a> was
the only useful blog post I’ve been able to find.</p>

<h2 id="dns-forwarding">DNS Forwarding</h2>

<p>At time of writing, Azure does not have a first-class DNS forwarder
service.  Therefore, I have to build this component.  Fortunately, it
is straightforward to build a DNS forwarder using a tiny VM and DNSMasq.</p>

<p>The solution looks like this:</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/azure_site2site/Azure-Site-to-Site.drawio.png" alt="High Level Networking Overview." />
  <figcaption>High Level Networking Overview.</figcaption>
</figure>
</div>

<p>In Azure, I create a private zone (azure.private.opequon.net) and link
it to my VNET.  With that, hosts inside the VNET can query the Azure
DNS servers and resolve hostnames in my private zone.</p>

<p>However, hosts outside of the VNET, like my on-premise network, cannot
access Azure DNS!</p>

<p>Having the DNS Forwarder VM allows me to, essentially, proxy DNS
queries between my on-premise FreeIPA and Azure DNS.</p>

<p>So that my Azure VMs also can resolve on-premise names.  The DNS
Forwarder VM also sends DNS queries back to FreeIPA for certain zones.
To ensure all Azure VMs have this by default, the VNETs primary DNS
server is changed from the default Azure DNS to my DNS Forwarder.</p>

<h1 id="lets-set-everything-up">Let’s set everything up!</h1>

<h2 id="manual-steps">Manual Steps</h2>

<p>Most things are covered by the automation, but a few items I had to
manually configure due to lack of available automation modules and/or
APIs.</p>

<p>Both of these items are one time setup, and neither incure a cloud cost.</p>

<h3 id="router-setup">Router Setup</h3>

<p>My Libreswan endpoint is within my network and thus is behind my
router.  Therefore, I need to configure my router to forward certain
ports to my Libreswan host.</p>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Protocol</th>
      <th>Destination</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4500</td>
      <td>UDP</td>
      <td>Libreswan Host</td>
    </tr>
    <tr>
      <td>4500</td>
      <td>TCP</td>
      <td>Libreswan Host</td>
    </tr>
    <tr>
      <td>500</td>
      <td>UDP</td>
      <td>Libreswan Host</td>
    </tr>
  </tbody>
</table>

<p>If you Libreswan endpoint is at the edge of your network (i.e. it
holds a public IP address), then this step is not necessary.</p>

<h3 id="freeipa-forward-zones">FreeIPA Forward Zones</h3>

<p>At time of writing, there are no Ansible modules allow configuration
of Forward Zones in FreeIPA/IdM.  I may just have missed it, not sure.</p>

<p>For these examples to make sense, here is my relevant configuration:</p>
<ul>
  <li>DNS Forwarder VM IP address:  172.31.225.4</li>
  <li>FreeIPA/IdM Server Hostname:  ipa.private.opequon.net</li>
  <li>Azure subdomain:  azure.private.opequon.net</li>
</ul>

<p>First, I create a DNS Forward Zone in FreeIPA with a forward policy of
‘only’.  This ensures that queries for <code class="language-plaintext highlighter-rouge">azure.private.opequon.net</code> are
forwarded to the DNS Forwarder VM.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@ipa ~]# ipa dnsforwardzone-add --forward-policy=only --forwarder=172.31.225.4 azure.private.opequon.net.
Server will check DNS forwarder(s).
This may take some time, please wait ...
ipa: WARNING: DNS server 172.31.225.4: query 'azure.private.opequon.net. SOA': The DNS operation timed out after 10.0006103515625 seconds.
  Zone name: azure.private.opequon.net.
  Active zone: TRUE
  Zone forwarders: 172.31.225.4
  Forward policy: only
</code></pre></div></div>

<p>The warning about DNS operation timed out will occur if you’re
performing this step BEFORE provisioning the entire enclave with the
rest of the automation.  Ignore it.</p>

<p>Next, to make the forward zone effective, I need a “glue record” to
include it in my primary zone.  This is just a simple NS record that
points back to my IPA server.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@ipa ~]# ipa dnsrecord-add private.opequon.net azure --ns-rec=ipa.private.opequon.net.
  Record name: azure
  NS record: ipa.private.opequon.net.
</code></pre></div></div>

<p>Once the rest of the enclave is configured, this IdM configuration
should immediately (or pretty close to immediately) work.</p>

<h2 id="automation-code">Automation Code</h2>

<p>All the code described here is on  <a href="https://github.com/xlark/azure-site-to-site">GitHub</a></p>

<p>The automation I have for this is written in a combination of Terraform and Ansible.</p>

<p>Two main scripts call the pieces in unison:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">build.sh</code> - Creates all cloud infrastructure in Azure and setups Libreswan and the DNS Forwarder</li>
  <li><code class="language-plaintext highlighter-rouge">destroy.sh</code> - Tears everything down.</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">build.sh</code> and <code class="language-plaintext highlighter-rouge">destroy.sh</code> scripts assumes that the Site-to-Site
VPN tunnel is being configured on the same host that is running the
script.  Some modification of the Ansible will be required if you want
to run it from a different host.</p>

<p>Sometimes the Azure APIs are very slow and cause the Terraform code to
timeout!  Luckily, all this code is idempotent, so if that happens, it
can just be run again.</p>

<h3 id="terraform-files">Terraform Files</h3>

<p>I’ve attempted to format and organize the Terraform files in a way
that is easy to understand, rather than strictly to normal.</p>

<p>The main objects are in the following files:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">main.tf</code> - Objects for the site-to-site VPN</li>
  <li><code class="language-plaintext highlighter-rouge">subnets.tf</code> - Subnets and Network Security Groups</li>
  <li><code class="language-plaintext highlighter-rouge">dns.tf</code> - DNS Private Zone</li>
  <li><code class="language-plaintext highlighter-rouge">dns_forwarder.tf</code> - DNS Forwarder VM</li>
</ul>

<p>Then there are more normal Terraform files, that have what you’d expect.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">outputs.tf</code></li>
  <li><code class="language-plaintext highlighter-rouge">providers.tf</code></li>
  <li><code class="language-plaintext highlighter-rouge">variables.tf</code></li>
  <li><code class="language-plaintext highlighter-rouge">versions.tf</code></li>
</ul>

<h4 id="maintf">main.tf</h4>

<p><code class="language-plaintext highlighter-rouge">main.tf</code> contains the Site-to-Site VPN resources, and the bare
minimum pre-requisites.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_resource_group</code> defines the resource group.  Every Azure
resource by this automation will live in this resource group.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_virtual_network</code> defines our “Azure Virtual Network”, aka VNet,
to which we will add subnets.  The VNet has variables for the
overarching address space as well as DNS servers for everything.  In
this automation, I’m overriding the default Azure DNS with the
statically assigned IP address of our DNS Forwarder.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_subnet</code> creates the subnet required for the Site-to-Site VPN.
This is here, instead of in <code class="language-plaintext highlighter-rouge">subnets.tf</code>, because it’s required for
the VPN connection.  Also, it <em>must</em> be named <code class="language-plaintext highlighter-rouge">GatewaySubnet</code>,
otherwise dependant resources throw an error.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_local_network_gateway</code> provides details for our “Local”
network, meaning the “On-Premise” side of the Site-to-Site VPN tunnel.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_public_ip</code> provides a public IP address for the Azure side of
the Site-to-Site VPN tunnel.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_virtual_network_gateway</code> provides the Azure-side of the
Site-to-Site VPN tunnel.  It links together the Public IP address and
the Azure GatewaySubnet.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_virtual_network_gateway_connection</code> creates the VPN.  It
links together the <code class="language-plaintext highlighter-rouge">azurerm_virtual_network_gateway</code>,
<code class="language-plaintext highlighter-rouge">azurerm_local_network_gateway</code>, and the VPN Shared Key and creates
the VPN configuration on the Azure side.  Once this resource has
finished provisioning, it is possible to start LibreSwan on the
on-premise side and activate the connection.</p>

<h4 id="subnetstf">subnets.tf</h4>

<p><code class="language-plaintext highlighter-rouge">subnets.tf</code> contains all <code class="language-plaintext highlighter-rouge">azurerm_subnet</code> definitions, except for the
<code class="language-plaintext highlighter-rouge">GatewaySubnet</code>.</p>

<p>In the public repo, I have just one subnet for my “main” network.  In
my personal lab, I add separate subnets for OpenShift and other things
I run in the cloud.</p>

<h4 id="dnstf">dns.tf</h4>

<p><code class="language-plaintext highlighter-rouge">dns.tf</code> does some minimal DNS work.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_private_dns_zone</code> actually creates our private DNS zone in
Azure.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_private_dns_zone_virtual_network_link</code> then links that DNS
zone to our VNet.  I have the <code class="language-plaintext highlighter-rouge">registration_enabled</code> flag set to true,
so all Virtual Machines connected to the VNet will automatically have
their hostnames registered in our private zone.</p>

<h4 id="dns_forwardertf">dns_forwarder.tf</h4>

<p><code class="language-plaintext highlighter-rouge">dns_forwarder.tf</code> defines the Virtual Machine that will be our DNS forwarder.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_network_interface</code> defines it’s network interface and,
importantly, its static IP within the VNet.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_linux_virtual_machine</code> is the virtual machine definition.
Currently uses a RHEL 8 VM, but this could be changed to almost any
RHEL-like VM and still work.</p>

<p><code class="language-plaintext highlighter-rouge">azurerm_network_security_group</code> is a simple network security group
for the instance.</p>

<h3 id="ansible-playbooks">Ansible Playbooks</h3>

<p>There are three Ansible playbooks.  The <code class="language-plaintext highlighter-rouge">build.sh</code> and <code class="language-plaintext highlighter-rouge">destroy.sh</code>
scripts pass the outputs of the Terraform build into these, so they
have no separate inventory or variable files.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">azure_dns_forwarder.yaml</code> - Configures the DNS forwarder</li>
  <li><code class="language-plaintext highlighter-rouge">azure_ipsec.yaml</code> - Configures on localhost, the on-premise side of the VPN tunnel</li>
  <li><code class="language-plaintext highlighter-rouge">azure_ipsec_remove.yaml</code> - Destroyes the on-premise side of the VPN tunnel</li>
</ul>

<h3 id="sample-terraformtfvars">Sample terraform.tfvars</h3>

<p>A sample terraform.tfvars is included.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># What Azure Region to use for all resources
azure_region_name="eastus"

# Resource group for all resources
resource_group_name="ENCLAVE-EAST"

# Default name for most resources
default_resource_name="ENCLAVE-EAST"

# Address Space and DNS servers for VNET
vnet_address_space=["172.31.224.0/19"]
vnet_dns_servers=["172.31.225.4"]

# Subnet for the Azure side of the Site-to-Site VPN
gateway_subnet=["172.31.224.0/24"]

# Names for Local Network Gateway and resources related to
# the on-premise side of the Site-to-Site VPN
local_network_gateway_name="ONPREM"
on_premise_name="ONPREM"

# On-Premise network range and public IP.
on_premise_private_network_ranges=["172.31.0.0/19"]
on_premise_public_ip_address="108.56.139.185"

# DNS Zone for Azure Private DNS
private_dns_zone_name="azure.private.opequon.net"

# Static Private IP address of DNS Forwarder.
dns_forwarder_ip_address="172.31.225.4"

# Name for thePublic IP address for the Azure side of the VPN
vpn_azure_public_ip_name="VPN-EAST"

# Name and range of main subnet in Azure
subnet_main_name = "EAST-MAIN"
subnet_main_cidr_ranges = ["172.31.225.0/24"]

# DNS information for DNS Forwarder VM
on_premise_dns_zones = ["private.opequon.net", "0.31.172.in-addr.arpa"]
on_premise_dns_server = "172.31.0.101"
# The Azure Private DNS server is a static IP address
# See:  https://docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-name-resolution-for-vms-and-role-instances#considerations
azure_private_dns_server = "168.63.129.16"
# The reverse DNS doesn't work, currently, for on-prem resolving Azure PTR records.
vnet_dns_reverse_zones = ["225.31.172.in-addr.arpa"]

# Shared Key for VPN
# DO NOT CHECK THIS INTO GIT, it's only here to show what the variable name is!
vpn_shared_key="mysecretkey"
</code></pre></div></div>

<h1 id="problems-and-improvements">Problems and Improvements</h1>

<p>There are still problems with this.  Maybe I will solve and update
when I have the time.</p>

<h2 id="lack-of-security-groups">Lack of security groups</h2>

<p>This configuration is pretty wide open internally.  I’m doing very
little segregation of Azure subnets that would be common in an
Enterprise organziation.</p>

<p>I still feel pretty safe with this configuration, because it’s only
really accessible from my home network, but if using this as a pattern
for something more serious.  It’s worth considering additional network
segmentation.</p>

<h2 id="reverse-dns">Reverse DNS</h2>

<p>Try as I might, I cannot get Reverse DNS forwarding to Azure private
DNS working with FreeIPA/IdM.</p>

<p>When I set up a forward zone in FreeIPA/IdM, it expects that the
destination DNS to have a SOA record equivalent to the forward zone.
So, if my forward zone is 225.31.172.in-addr.arpa, then the DNS server
I’m forwarding to is expected to have an SOA record for
225.31.172.in-addr.arpa.</p>

<p>Azure DNS appears to dump all PTR records into a big in-addr.arpa
zone.  So, there are no appropriate SOA records and FreeIPA/IdM
refuses to set up the forward zone.</p>

<p>Other DNS implementations, like dnsmasq, can cope with this.  I
suspect because they textually parsing the DNS query at the server
before sending it on, but I’m not entirely sure.  A bit more research
is required.</p>

<p>At any rate, Reverse DNS does not automatically work with this
solution, which is a problem.</p>

<h2 id="plain-text-vpn-key">Plain text VPN key</h2>

<p>This example just has the shared VPN key as plain text.  For a home
lab, this is <em>mostly</em> acceptable, although still not ideal.  For an
enterprise solution, this VPN key should be vaulted and highly
protected.</p>

<h2 id="routing-weirdness">Routing weirdness</h2>

<p>In this example, the IPSec tunnel is not at the edge of the network,
but is instead inside my private network.  My consumer router forwards
certain ports to the IPSec host in order to make the connection work.</p>

<p>When I’m on the road (a rarity these days), I use the VPN
client/server provided by my consumer router, and there are routing
difficulties when trying to use resources in the enclave when I’m
connected to my router’s VPN.</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="linuxunix" /><category term="linux" /><category term="cloud" /><category term="azure" /><category term="vpn" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Fedora Copr and Custom Gem RPMs for Fedora</title><link href="/blog/linuxunix/2022/01/11/copr-and-gemfiles.html" rel="alternate" type="text/html" title="Fedora Copr and Custom Gem RPMs for Fedora" /><published>2022-01-11T17:42:00-05:00</published><updated>2022-01-11T17:42:00-05:00</updated><id>/blog/linuxunix/2022/01/11/copr-and-gemfiles</id><content type="html" xml:base="/blog/linuxunix/2022/01/11/copr-and-gemfiles.html"><![CDATA[<p>This blog is formatted using Jekyll and I use the
<code class="language-plaintext highlighter-rouge">jekyll-redirect-from</code> plugin to do URL redirection, so that old blog
posts migrated my former blog platform can still be accessed via the
same URL.</p>

<p>My regular desktop operating system and Jekyll has been packaged by
Fedora for some years.  However, most plugins, including
<code class="language-plaintext highlighter-rouge">jekyll-redirect-from</code>, are NOT packaged by Fedora.</p>

<p>In the past, this has led me to have to muck about with other package
tools, like gem or pip.  I don’t like it.  They put stuff in weird
places, duplicate system provided dependencies and just cause a mess.</p>

<p>No…call me stubborn, but I’d just like a nice simple RPM packages.
Thank you very much.</p>

<p>Enter <a href="https://copr.fedorainfracloud.org/">Fedora Copr</a>.  Copr is a
front-end to Fedora’s package build system that allows one to build
custom RPM packages and host them in a personal, publicly-accesible,
yum repositories.</p>

<p>Copr has been around for a while, and I play around building some
Emacs packages about 4 years ago, but it’s become substantially
easier.</p>

<p>In just a few commands, I can have the Jekyll plugin I want, or really
most Gems, converted to an RPM and available for me to install.</p>

<h1 id="setup">Setup</h1>

<p>First, in order to use Copr, a Fedora Account is required.  <a href="https://accounts.fedoraproject.org/">Signing
up</a> for one is free and
relatively easy.</p>

<p>Next, I’d highly recommend getting the <code class="language-plaintext highlighter-rouge">copr-cli</code> package installed on
your Fedora machine.  It’s part of the default Fedora repositories, so
it should be as simple as <code class="language-plaintext highlighter-rouge">dnf install copr-cli</code>.</p>

<p>Almost all of the tasks could be done through the Copr Web UI, but in
this post, I’m using <code class="language-plaintext highlighter-rouge">copr-cli</code>.</p>

<p>Next, populate the Copr token by visiting
<a href="https://copr.fedorainfracloud.org/api/">https://copr.fedorainfracloud.org/api/</a></p>

<p>Put the output there into <code class="language-plaintext highlighter-rouge">~/.config/copr</code>.  It should look something
like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[copr-cli]
login = gibberish
username = your_user_name
token = gibberish
copr_url = https://copr.fedorainfracloud.org
# expiration date: 2022-07-09
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">copr whoami</code> should now return your Fedora Account name:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[chale@work-wired ~]$ copr whoami
xlark
</code></pre></div></div>

<h1 id="creating-a-project">Creating a project</h1>

<p>The first step is to create a project.  This acts as your yum
repository.  It’s possible to create multiple projects, if desired.</p>

<p>For example, I have an <code class="language-plaintext highlighter-rouge">emacs</code> project for Emacs related packages, and
I created a <code class="language-plaintext highlighter-rouge">jekyll-gems</code> project for all the Jekyll Gems I intend to
use:</p>

<p>To create a project named <code class="language-plaintext highlighter-rouge">jekyll-gems</code>, run the following command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[chale@work-wired ~]$ copr-cli create --chroot fedora-35-x86_64 jekyll-gems
New project was successfully created.
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--chroot fedora-35-x86_64</code> specificies the package build
environment.  There are many different options.  Fedora 35 seemed
reasonable to me, since I’m currently using it, although rawhide may
be a more future proof selection.</p>

<h1 id="building-the-package">Building the Package</h1>

<p>Now that we have a project, building a package from a Gem file in just
one command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[chale@work-wired ~]$ copr-cli buildgem --gem jekyll-redirect-from jekyll-gems
Build was added to jekyll-gems:
  https://copr.fedorainfracloud.org/coprs/build/3144944
Created builds: 3144944
Watching build(s): (this may be safely interrupted)
  17:17:28 Build 3144944: pending
  17:17:58 Build 3144944: running
  17:20:28 Build 3144944: succeeded
</code></pre></div></div>

<p>This builds an RPM from the Gem, <code class="language-plaintext highlighter-rouge">jekyll-redirect-from</code> into the project
<code class="language-plaintext highlighter-rouge">jekyll-gems</code>.</p>

<p>Since, <code class="language-plaintext highlighter-rouge">jekyll-redirect-from</code> is hosted on
<a href="https://rubygems.org/">RubyGems</a>, Copr pulls the Gem file directly
from there without any additional information required.</p>

<p>After the build has succeeded, the package is ready to install!</p>

<h1 id="installing-the-package">Installing the Package</h1>

<p>First enable your Copr repository.  The name should always be in the form of “Fedora Account/Project Name”:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[chale@work-wired ~]$ sudo dnf copr enable xlark/jekyll-gems 
Enabling a Copr repository. Please note that this repository is not part
of the main distribution, and quality may vary.

The Fedora Project does not exercise any power over the contents of
this repository beyond the rules outlined in the Copr FAQ at
&lt;https://docs.pagure.org/copr.copr/user_documentation.html#what-i-can-build-in-copr&gt;,
and packages are not held to any quality or security level.

Please do not file bug reports about these packages in Fedora
Bugzilla. In case of problems, contact the owner of this repository.

Do you really want to enable copr.fedorainfracloud.org/xlark/jekyll-gems? [y/N]: y
Repository successfully enabled.
</code></pre></div></div>

<p>After that, we can just install the package like normal!  By Fedora
standards, all RPMs for Gems are always prefixed by “rubygem-“</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[chale@work-wired ~]$ sudo dnf install rubygem-jekyll-redirect-from
Copr repo for jekyll-gems owned by xlark                                                              15 kB/s | 2.6 kB     00:00    
Dependencies resolved.
=====================================================================================================================================
 Package                            Architecture Version                Repository                                              Size
=====================================================================================================================================
Installing:
 rubygem-jekyll-redirect-from       noarch       0.16.0-1.fc35          copr:copr.fedorainfracloud.org:xlark:jekyll-gems        14 k

Transaction Summary
=====================================================================================================================================
Install  1 Package

Total download size: 14 k
Installed size: 9.8 k
Is this ok [y/N]: 
</code></pre></div></div>
<h1 id="sharing-your-project">Sharing your project</h1>

<p>Projects and their corresponding repositories created in Copr are
public.  Anyone running Fedora can enable your repository using <code class="language-plaintext highlighter-rouge">dnf
copr enable</code>.</p>

<p>Others can benefit from whatever you package using Copr, so if you
package something useful, feel free to share with others!</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="linuxunix" /><category term="fedora" /><category term="linux" /><category term="x86" /><category term="rpm" /><category term="copr" /><summary type="html"><![CDATA[This blog is formatted using Jekyll and I use the jekyll-redirect-from plugin to do URL redirection, so that old blog posts migrated my former blog platform can still be accessed via the same URL.]]></summary></entry><entry><title type="html">Resurrecting the VAXen, part 3: Hardware Repairs</title><link href="/blog/vintagecomputing/2021/05/29/resurrecting-the-vaxen-part-3-repairs.html" rel="alternate" type="text/html" title="Resurrecting the VAXen, part 3: Hardware Repairs" /><published>2021-05-29T17:30:00-04:00</published><updated>2021-05-29T17:30:00-04:00</updated><id>/blog/vintagecomputing/2021/05/29/resurrecting-the-vaxen-part-3-repairs</id><content type="html" xml:base="/blog/vintagecomputing/2021/05/29/resurrecting-the-vaxen-part-3-repairs.html"><![CDATA[<p>This post is part of an on-going, multi-decade series on my
half-hearted attempts to get all my VAX hardware up and running.</p>
<ul>
  <li><a href="/blog/vintagecomputing/2015/10/01/resurrecting-the-vaxen-part-1.html">Resurrecting the VAXen, part 1</a></li>
  <li><a href="/blog/vintagecomputing/2015/10/02/resurrecting-the-vaxen-part-2-scsi2sd.html">Resurrecting the VAXen, part 2:  SCSI2SD</a></li>
  <li><a href="/blog/vintagecomputing/2021/05/29/resurrecting-the-vaxen-part-3-repairs.html">Resurrecting the VAXen, part 3:  Hardware Repairs</a></li>
</ul>

<p>Start and stop is the nature of a lot of personal projects.  I started
to get my VAXen up and running several years ago and then, had to stop
for other life reasons.</p>

<p>My condo was remodeled and I’m having to reassemble my lab, so it
seems like a good time to restart the project.</p>

<p>First thing first was testing all the VAXen on taking them out of
storage.  All booted and got to the boot monitor, except for my
VAXstation 4000/90.  For the entire time I’ve owned it, this poor VAX
has never booted and I never dedicated the time to diagnosing it.
This is my most powerful VAX, so I’ve always wanted to restore it to a
working state.</p>

<p>Turns out, there were two major issues with this VAX:</p>

<ul>
  <li>A Damaged SIMM socket, presumably preventing memory</li>
  <li>A dead clock battery</li>
</ul>

<p>VAXen have diagnostic lights to assist in boot up diagnostics.  This
VAX had all eight lights lit steady on bootup, and would not progress
from there.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/vax/eight_lights.jpg" alt="There are eight lights!" />
  <figcaption>There are eight lights!</figcaption>
</figure>
</div>

<p>According to the <a href="http://www.vaxarchive.org/hardware/vs3100/3100leds.html">VAXArchive</a>, this means
“Power is applied, but no instruction is executed”; essentially, no
instructions have been executed by the CPU.  During normal startup,
this is the first light combination displayed, but a healthy VAX
quickly progresses to other light sequences.</p>

<p>Staying in this state, per my research, generally indicates a memory
issue or an issue with the real time clock.</p>

<p>First, I experimented with the memory.  Maybe I had a bad module?
Luckily, my VAXstation 4000/60 is functional and takes the same SIMMs
as the 4000/90.</p>

<p>To my knowledge there are 2 sizes of SIMM modules available for
VAXstation 4000s: 4MB and 16MB.  These are fairly easy to distinguish
based on the labels affixed to them. AA mean 4MB, CA means 16MB</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/vax/simms.jpg" alt="SIMM Differences." />
  <figcaption>SIMM Differences.</figcaption>
</figure>
</div>

<p>For the 4000/90, these need to be installed in sets of four.  The
4000/90 has 8 slots and no on-board RAM, where as the 4000/60 has 6
slots and 8MB of onboard RAM.</p>

<p>The 4000/60 and the 4000/90 are different in how SIMMs must be
installed and the differences are not immediately intuitive.
Fortunately, the
<a href="https://vt100.net/manx/details/1,2820">service manual</a> is available
in the <a href="https://vt100.net/manx/">MANX Archive</a>.</p>

<p>For the 4000/60, matching pairs must be installed with the
lower capacity SIMMs in lower numbered banks.</p>

<p>For the 4000/90, matching sets of 4 must be installed in a staggered
pattern.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/vax/vs4k60vs4k90simms.jpg" alt="4000/60 vs 4000/90 SIMM Arrangement." />
  <figcaption>4000/60 vs 4000/90 SIMM Arrangement.</figcaption>
</figure>
</div>

<p>All of the RAM from the 4000/90 tested good in the 4000/60, so all of
the SIMM modules were good.</p>

<p>My 4000/90 is “fully loaded”, and it has always had all SIMM spots
occupied.  So, knowing that the SIMMs were good, I loaded only one
set of four.</p>

<p>On power up, I got the boot monitor!</p>

<p>This is the first time this machine has booted in the decade I’ve owned it!</p>

<p>The culprit here was a damaged SIMM socket, it’s very difficult to get
a picture of this, but I’ve tried.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/vax/simm_damage.jpg" alt="SIMM Damage." />
  <figcaption>SIMM Damage.</figcaption>
</figure>
</div>

<p>I’ve ordered, what may be a replacement on <a href="https://www.digikey.com/en/products/detail/te-connectivity-amp-connectors/5822030-4/2262342">Digi-Key</a>.</p>

<p>Now, this solved some of the problems, but it was not always reliably
booting.  If I started it completely cold, it would still be stuck
with all diagnostic LEDs illuminated.  However, a quick power cycle
after a few minutes of running would reliably get the machine to the
boot monitor.</p>

<p>This has to be the real time clock, as I’ve seen reports that a dead
real time clock will cause this symptom.  The battery is, I presume,
the original and does not hold a charge.  Interestingly enough, the
4000/60 does not seem to be affected by this, even though it’s a
similar, but not identical, system board.</p>

<p>Most DEC products of this era use the DS1287 RTC chip.  This chip
contains both the circuitry for the clock, as well as a built in
battery.  These have been out of production for years, and many people
have resorted to hacking them to replace the built in battery.</p>

<div class="cfhimage">
<figure class="image">
  <img src="/static_files/vax/dallas.jpg" alt="DS1287A and Compatibles." />
  <figcaption>DS1287A and Compatibles.</figcaption>
</figure>
</div>

<p>I can’t be arsed to do something like that, especially because there
are plug-compatible parts stil being produced.  The DS12887+ and
DS12887A+ are produced by Maxim Integrated Products, and I’ve used
them before in PCs and other systems that use the DS1287.  I figured I
might as well replace ALL of my DS1287’s so I’ve ordered several from
<a href="https://www.digikey.com/en/products/detail/maxim-integrated/DS12887-/956874">Digi-Key</a>
At over $10 a pop, they are not as economical as coin cells, but it
beats having to do some time consuming modification to the either the
DS1287 chip or the board.</p>

<p>The last remaining issue is that the SCSI2SD card that I’m using with
the VAXes always throws an error on boot.  I can later issue <code class="language-plaintext highlighter-rouge">boot
dka0</code> from the system monitor, but this error prevents the VAX from
auto-booting.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?? 001  10      SCSI  0048
</code></pre></div></div>

<p>Currently, I can’t seem to find any information for this error, but
that’s something for another day.</p>]]></content><author><name>Clark Hale</name></author><category term="blog" /><category term="vintagecomputing" /><category term="hardware" /><category term="vax" /><category term="dec" /><category term="vms" /><category term="openvms" /><summary type="html"><![CDATA[This post is part of an on-going, multi-decade series on my half-hearted attempts to get all my VAX hardware up and running. Resurrecting the VAXen, part 1 Resurrecting the VAXen, part 2: SCSI2SD Resurrecting the VAXen, part 3: Hardware Repairs]]></summary></entry></feed>