<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://kimcheegi.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kimcheegi.github.io/" rel="alternate" type="text/html" /><updated>2026-07-30T13:02:40+00:00</updated><id>https://kimcheegi.github.io/feed.xml</id><title type="html">KimcheeGI Labs</title><subtitle>Automating Cybersecurity at Browser Speed</subtitle><entry><title type="html">Building an Oracle Linux 9 Packer VM on Windows: Lessons Learned</title><link href="https://kimcheegi.github.io/2026/07/30/building-oracle-linux-9-packer-vm-lessons-learned.html" rel="alternate" type="text/html" title="Building an Oracle Linux 9 Packer VM on Windows: Lessons Learned" /><published>2026-07-30T00:00:00+00:00</published><updated>2026-07-30T00:00:00+00:00</updated><id>https://kimcheegi.github.io/2026/07/30/building-oracle-linux-9-packer-vm-lessons-learned</id><content type="html" xml:base="https://kimcheegi.github.io/2026/07/30/building-oracle-linux-9-packer-vm-lessons-learned.html"><![CDATA[<h1 id="building-an-oracle-linux-9-packer-vm-on-windows-lessons-learned">Building an Oracle Linux 9 Packer VM on Windows: Lessons Learned</h1>

<p>What looks like a straightforward Packer build — boot from ISO, run kickstart, SSH in, done — turned into a four-day deep-dive when every assumption about the Oracle Linux 9 installer, RHEL9’s SSH daemon configuration, and Ansible module resolution turned out to be wrong. This post documents every failure, the root cause, and the exact fix.</p>

<p>The goal was a fully automated VMware Workstation VM build on Windows using Packer, producing a hardened Oracle Linux 9 Ansible controller that can operate in an air-gapped environment.</p>

<hr />

<h2 id="the-stack">The Stack</h2>

<ul>
  <li><strong>Host:</strong> Windows 11, VMware Workstation 17.5</li>
  <li><strong>Packer:</strong> v1.16.0 with <code class="language-plaintext highlighter-rouge">github.com/hashicorp/vmware</code> plugin ≥ 1.0.0</li>
  <li><strong>Guest OS:</strong> Oracle Linux 9.8 (<code class="language-plaintext highlighter-rouge">OracleLinux-R9-U8-x86_64-dvd.iso</code>)</li>
  <li><strong>Purpose:</strong> Ansible controller for STIG automation in air-gapped environments</li>
</ul>

<hr />

<h2 id="problem-1-anaconda-never-saw-the-kickstart--boot_wait--60s">Problem 1: Anaconda never saw the kickstart — <code class="language-plaintext highlighter-rouge">boot_wait = "60s"</code></h2>

<h3 id="symptom">Symptom</h3>

<p>The VM booted, Anaconda loaded the graphical installer, and showed the Installation Summary screen with red “Kickstart insufficient!” warnings for Root Password and Installation Destination. After 13+ minutes Packer gave up waiting for SSH.</p>

<h3 id="root-cause">Root Cause</h3>

<p>The Oracle Linux 9 isolinux boot menu has a <strong>60-second countdown</strong> before it automatically boots the top entry. The Packer config had <code class="language-plaintext highlighter-rouge">boot_wait = "60s"</code> — meaning Packer waited the full 60 seconds before sending the <code class="language-plaintext highlighter-rouge">&lt;tab&gt;</code> keystroke. The menu had already timed out and auto-booted into a standard graphical install <em>without</em> the kickstart URL.</p>

<h3 id="fix">Fix</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">boot_wait</span> <span class="err">=</span> <span class="s2">"10s"</span>   <span class="c1"># catch the menu before it auto-boots</span>
<span class="nx">boot_command</span> <span class="err">=</span> <span class="p">[</span>
  <span class="s2">"&lt;tab&gt;&lt;wait&gt;"</span><span class="p">,</span>
  <span class="s2">" inst.ks=http://:/ks.cfg inst.text&lt;enter&gt;&lt;wait&gt;"</span>
<span class="p">]</span>
</code></pre></div></div>

<p>Two changes:</p>
<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">boot_wait = "10s"</code></strong> — sends the keystroke while the isolinux menu is still visible</li>
  <li><strong><code class="language-plaintext highlighter-rouge">inst.text</code></strong> — uses text-mode Anaconda, which is faster and more reliable for unattended installs</li>
</ol>

<hr />

<h2 id="problem-2-bios-vs-efi--firmware--bios-required">Problem 2: BIOS vs. EFI — <code class="language-plaintext highlighter-rouge">firmware = "bios"</code> required</h2>

<h3 id="symptom-1">Symptom</h3>

<p>Even after fixing <code class="language-plaintext highlighter-rouge">boot_wait</code>, the <code class="language-plaintext highlighter-rouge">&lt;tab&gt;</code> boot command was only working intermittently. On some runs the installer loaded correctly; on others it ignored the keystroke entirely.</p>

<h3 id="root-cause-1">Root Cause</h3>

<p>VMware Workstation creates VMs in EFI mode by default for <code class="language-plaintext highlighter-rouge">guest_os_type = "rhel9-64"</code>. EFI boots into GRUB2, which uses a completely different menu interaction model — pressing <code class="language-plaintext highlighter-rouge">&lt;tab&gt;</code> does nothing. The isolinux <code class="language-plaintext highlighter-rouge">&lt;tab&gt;</code> trick only works in <strong>BIOS/legacy</strong> mode.</p>

<h3 id="fix-1">Fix</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">firmware</span> <span class="err">=</span> <span class="s2">"bios"</span>
</code></pre></div></div>

<p>This forces the VM to boot in BIOS mode, which uses isolinux where <code class="language-plaintext highlighter-rouge">&lt;tab&gt;</code> edits the selected boot entry’s kernel command line.</p>

<hr />

<h2 id="problem-3-kickstart-insufficient--missing-explicit-rootpw-and-partitioning">Problem 3: Kickstart “insufficient” — missing explicit <code class="language-plaintext highlighter-rouge">rootpw</code> and partitioning</h2>

<h3 id="symptom-2">Symptom</h3>

<p>Anaconda showed the installation summary screen with red icons on “Root Password” and “Installation Destination” even when the kickstart URL was correctly injected.</p>

<h3 id="root-cause-2">Root Cause</h3>

<p>The early kickstart file used <code class="language-plaintext highlighter-rouge">autopart --type=lvm</code>, which Anaconda 34 (OL9) sometimes fails to parse correctly, and was missing an explicit <code class="language-plaintext highlighter-rouge">rootpw</code> directive entirely. Anaconda treats both as “not configured” and requires manual completion.</p>

<h3 id="fix-2">Fix</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rootpw --plaintext CyberSecurity123!
zerombr
clearpart --all --initlabel --disklabel=msdos
part / --fstype=ext4 --size=1 --grow
</code></pre></div></div>

<p>Simple ext4 over a single MBR partition. No LVM, no complexity. Anaconda handles it without hesitation.</p>

<hr />

<h2 id="problem-4-ssh-authentication-failure-after-successful-install--ol9s-sshd_configd-override">Problem 4: SSH authentication failure after successful install — OL9’s <code class="language-plaintext highlighter-rouge">sshd_config.d</code> override</h2>

<h3 id="symptom-3">Symptom</h3>

<p>After a successful 13-minute installation and reboot, Packer connected to SSH and immediately got:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh: unable to authenticate, attempted methods [none publickey password],
no supported methods remain
</code></pre></div></div>

<p>Password auth was definitely enabled — it was in the kickstart <code class="language-plaintext highlighter-rouge">%post</code>. The root password was definitely set.</p>

<h3 id="root-cause-3">Root Cause</h3>

<p>Oracle Linux 9 (and all RHEL 9 derivatives) ship a <strong>drop-in override file</strong> at <code class="language-plaintext highlighter-rouge">/etc/ssh/sshd_config.d/50-redhat.conf</code> that sets:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PermitRootLogin prohibit-password
PasswordAuthentication no
</code></pre></div></div>

<p>Files in <code class="language-plaintext highlighter-rouge">sshd_config.d/</code> with a <strong>lower</strong> filename prefix take precedence over those with a higher one, and <strong>all drop-ins override the main <code class="language-plaintext highlighter-rouge">sshd_config</code></strong>. Editing <code class="language-plaintext highlighter-rouge">/etc/ssh/sshd_config</code> with <code class="language-plaintext highlighter-rouge">sed</code> had no effect — the drop-in was winning.</p>

<h3 id="fix-3">Fix</h3>

<p>Add a <code class="language-plaintext highlighter-rouge">01-packer.conf</code> drop-in in the kickstart <code class="language-plaintext highlighter-rouge">%post</code> section. Files prefixed <code class="language-plaintext highlighter-rouge">01-</code> load before <code class="language-plaintext highlighter-rouge">50-redhat.conf</code> and override it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /etc/ssh/sshd_config.d
<span class="nb">cat</span> <span class="o">&gt;</span> /etc/ssh/sshd_config.d/01-packer.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
PermitRootLogin yes
PasswordAuthentication yes
</span><span class="no">EOF
</span><span class="nb">chmod </span>600 /etc/ssh/sshd_config.d/01-packer.conf
</code></pre></div></div>

<hr />

<h2 id="problem-5-packer-provisioner-calling-yum--y-update--network-blocked">Problem 5: Packer provisioner calling <code class="language-plaintext highlighter-rouge">yum -y update</code> — network blocked</h2>

<h3 id="symptom-4">Symptom</h3>

<p>After SSH finally connected, the Packer shell provisioner ran <code class="language-plaintext highlighter-rouge">yum -y update</code> and failed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Curl error (7): Couldn't connect to server for
https://yum.oracle.com/repo/OracleLinux/OL9/appstream/x86_64/...
[Failed to connect to yum.oracle.com port 443: Connection refused]
</code></pre></div></div>

<h3 id="root-cause-4">Root Cause</h3>

<p>This is an air-gapped build environment. The provisioner was trying to reach Oracle’s public yum servers which are blocked.</p>

<h3 id="fix-4">Fix</h3>

<p>Remove the <code class="language-plaintext highlighter-rouge">yum -y update</code> provisioner entirely. The kickstart already installs everything needed from the local ISO during Anaconda installation (<code class="language-plaintext highlighter-rouge">@core</code>, <code class="language-plaintext highlighter-rouge">@system-tools</code>, <code class="language-plaintext highlighter-rouge">@standard</code>, <code class="language-plaintext highlighter-rouge">python3</code>, <code class="language-plaintext highlighter-rouge">ansible-core</code> via pip3). The Packer provisioner was redundant.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">provisioner</span> <span class="s2">"shell"</span> <span class="p">{</span>
  <span class="nx">inline</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"echo '=== Build verification ==='"</span><span class="p">,</span>
    <span class="s2">"python3 --version"</span><span class="p">,</span>
    <span class="s2">"ansible --version | head -1"</span><span class="p">,</span>
    <span class="s2">"echo '=== Build complete ==='"</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="problem-6-ansible-playbook--firewalld-module-not-found">Problem 6: Ansible playbook — <code class="language-plaintext highlighter-rouge">firewalld</code> module not found</h2>

<h3 id="symptom-5">Symptom</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR! couldn't resolve module/action 'firewalld'.
</code></pre></div></div>

<h3 id="root-cause-5">Root Cause</h3>

<p><code class="language-plaintext highlighter-rouge">ansible-core</code> (installed via pip) does not include the <code class="language-plaintext highlighter-rouge">firewalld</code> module. It belongs to the <code class="language-plaintext highlighter-rouge">ansible.posix</code> collection which must be installed separately.</p>

<h3 id="fix-5">Fix</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-galaxy collection <span class="nb">install </span>ansible.posix
</code></pre></div></div>

<p>And in the playbook, use the fully-qualified collection name:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Allow http/https through firewalld</span>
  <span class="na">ansible.posix.firewalld</span><span class="pi">:</span>
    <span class="na">service</span><span class="pi">:</span> <span class="s">https</span>
    <span class="na">permanent</span><span class="pi">:</span> <span class="s">yes</span>
    <span class="na">state</span><span class="pi">:</span> <span class="s">enabled</span>
  <span class="na">ignore_errors</span><span class="pi">:</span> <span class="s">yes</span>
</code></pre></div></div>

<hr />

<h2 id="problem-7-lookup-is-not-failed--invalid-jinja2-test">Problem 7: <code class="language-plaintext highlighter-rouge">lookup(...) is not failed</code> — invalid Jinja2 test</h2>

<h3 id="symptom-6">Symptom</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The conditional check 'lookup('file', '/opt/offline-assets/assets.tar.gz',
errors='ignore') is not failed' failed.
The error was: The 'failed' test expects a dictionary
</code></pre></div></div>

<h3 id="root-cause-6">Root Cause</h3>

<p>The <code class="language-plaintext highlighter-rouge">is failed</code> / <code class="language-plaintext highlighter-rouge">is not failed</code> Jinja2 tests are designed for <strong>task result dictionaries</strong> (objects with a <code class="language-plaintext highlighter-rouge">failed</code> key), not for strings. <code class="language-plaintext highlighter-rouge">lookup('file', ...)</code> returns a string (file contents) or empty string on error — applying <code class="language-plaintext highlighter-rouge">is not failed</code> to a string throws a type error.</p>

<h3 id="fix-6">Fix</h3>

<p>Replace all <code class="language-plaintext highlighter-rouge">lookup('file', PATH, errors='ignore') is not failed</code> conditions with proper <code class="language-plaintext highlighter-rouge">stat</code> module pre-checks:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Add before the block:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check for offline bundle</span>
  <span class="na">stat</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/opt/offline-assets/assets.tar.gz</span>
  <span class="na">register</span><span class="pi">:</span> <span class="s">offline_bundle_stat</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Import offline asset bundle (if present)</span>
  <span class="na">block</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ensure offline folder exists</span>
      <span class="na">file</span><span class="pi">:</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/opt/offline-assets</span>
        <span class="na">state</span><span class="pi">:</span> <span class="s">directory</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Unpack offline bundle</span>
      <span class="na">unarchive</span><span class="pi">:</span>
        <span class="na">src</span><span class="pi">:</span> <span class="s">/opt/offline-assets/assets.tar.gz</span>
        <span class="na">dest</span><span class="pi">:</span> <span class="s">/opt/offline-assets/</span>
        <span class="na">remote_src</span><span class="pi">:</span> <span class="s">yes</span>
  <span class="na">when</span><span class="pi">:</span> <span class="s">offline_bundle_stat.stat.exists</span>
</code></pre></div></div>

<p>Six occurrences of this pattern existed across <code class="language-plaintext highlighter-rouge">controller.yml</code> and <code class="language-plaintext highlighter-rouge">securityonion.yml</code>. All replaced.</p>

<hr />

<h2 id="problem-8-import_role-with-when--condition-ignored-at-parse-time">Problem 8: <code class="language-plaintext highlighter-rouge">import_role</code> with <code class="language-plaintext highlighter-rouge">when:</code> — condition ignored at parse time</h2>

<h3 id="symptom-7">Symptom</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR! the role 'stig-manager' was not found in ...
</code></pre></div></div>

<p>Even though the task had <code class="language-plaintext highlighter-rouge">when: have_stig_manager</code> and <code class="language-plaintext highlighter-rouge">have_stig_manager</code> was <code class="language-plaintext highlighter-rouge">false</code>.</p>

<h3 id="root-cause-7">Root Cause</h3>

<p><code class="language-plaintext highlighter-rouge">import_role</code> is <strong>statically resolved at parse time</strong>, before any <code class="language-plaintext highlighter-rouge">when:</code> conditions are evaluated. Ansible tries to load the role into the play graph immediately — and fails if the role directory doesn’t exist, regardless of the condition.</p>

<h3 id="fix-7">Fix</h3>

<p>Use <code class="language-plaintext highlighter-rouge">include_role</code> instead, which is <strong>dynamically resolved at runtime</strong> and correctly skips when the condition is false:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run stig-manager role if present</span>
  <span class="na">include_role</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">stig-manager"</span>
  <span class="na">when</span><span class="pi">:</span> <span class="s">have_stig_manager</span>
</code></pre></div></div>

<hr />

<h2 id="manual-intervention-steps-required">Manual Intervention Steps Required</h2>

<p>Even with a fully automated Packer build, the following steps currently require manual action:</p>

<h3 id="1-re-attach-the-iso-after-packer-completes">1. Re-attach the ISO after Packer completes</h3>

<p>Packer detaches the ISO at the end of the build (<code class="language-plaintext highlighter-rouge">Detaching ISO from CD-ROM device ide0:0</code>). If you need to install additional packages from the DVD (e.g., nginx for air-gapped deployments), you must re-attach it in VMware Workstation settings and reboot the VM — the kernel requires a reboot to detect new optical media in VMware.</p>

<h3 id="2-ssh-keyboard-input-from-vs-code-terminal">2. SSH keyboard input from VS Code terminal</h3>

<p>VS Code integrated terminals cannot forward keystrokes to interactive SSH password prompts. Solutions:</p>
<ul>
  <li><strong>SSH key auth</strong> — generate a key pair, paste the public key into the VM console, then use <code class="language-plaintext highlighter-rouge">-i keyfile</code> with scp/ssh</li>
  <li><strong>sshpass via WSL</strong> — <code class="language-plaintext highlighter-rouge">sshpass -p 'password' scp ...</code> works reliably</li>
  <li><strong>External terminal</strong> (Windows Terminal, cmd.exe) — handles interactive prompts fine</li>
</ul>

<h3 id="3-install-ansibleposix-collection-before-running-the-playbook">3. Install <code class="language-plaintext highlighter-rouge">ansible.posix</code> collection before running the playbook</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-galaxy collection <span class="nb">install </span>ansible.posix
</code></pre></div></div>

<p>The collection is not included with <code class="language-plaintext highlighter-rouge">ansible-core</code> and there is no offline-first fallback in the current playbook.</p>

<hr />

<h2 id="final-build-outcome">Final Build Outcome</h2>

<p>After all fixes, the Packer build completes in ~20 minutes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>==&gt; Connected to SSH!
==&gt; === Build verification ===
==&gt; Python 3.9.25
==&gt; ansible [core 2.15.13]
==&gt; === Build complete ===
Build finished after 20 minutes 7 seconds.
Artifacts: VM files in directory: output-oracle_linux
</code></pre></div></div>

<p>The controller playbook runs to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PLAY RECAP
localhost: ok=25  changed=8  failed=0  skipped=12  ignored=1
</code></pre></div></div>

<p>nginx is running as the HTTPS offline asset server, the ansible user is configured with passwordless sudo, and SELinux context is applied to <code class="language-plaintext highlighter-rouge">/opt/offline-assets</code>.</p>

<hr />

<h2 id="key-takeaways">Key Takeaways</h2>

<table>
  <thead>
    <tr>
      <th>Assumption</th>
      <th>Reality</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">boot_wait = "60s"</code> gives Anaconda time to load</td>
      <td>It gives the isolinux menu time to time out and auto-boot without the kickstart</td>
    </tr>
    <tr>
      <td>VMware Workstation uses BIOS by default</td>
      <td>It uses EFI for RHEL9; <code class="language-plaintext highlighter-rouge">&lt;tab&gt;</code> only works with isolinux (BIOS)</td>
    </tr>
    <tr>
      <td>Editing <code class="language-plaintext highlighter-rouge">/etc/ssh/sshd_config</code> enables root login</td>
      <td>OL9’s <code class="language-plaintext highlighter-rouge">50-redhat.conf</code> drop-in overrides it; write a <code class="language-plaintext highlighter-rouge">01-</code> prefixed drop-in instead</td>
    </tr>
    <tr>
      <td>Packer provisioners can update packages</td>
      <td>Not in air-gapped environments; do all installs in the kickstart from the local ISO</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">import_role</code> respects <code class="language-plaintext highlighter-rouge">when:</code></td>
      <td>It doesn’t — use <code class="language-plaintext highlighter-rouge">include_role</code> for conditional role execution</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lookup(...) is not failed</code> checks file existence</td>
      <td>It’s a type error; use the <code class="language-plaintext highlighter-rouge">stat</code> module</td>
    </tr>
  </tbody>
</table>

<hr />

<p>The full project is on GitHub: <a href="https://github.com/KimcheeGI/oracle-stig-so-platform">KimcheeGI/oracle-stig-so-platform</a></p>]]></content><author><name>Charles</name></author><category term="packer" /><category term="oracle-linux" /><category term="vmware" /><category term="kickstart" /><category term="ansible" /><category term="air-gapped" /><category term="stig" /><category term="devsecops" /><summary type="html"><![CDATA[Building an Oracle Linux 9 Packer VM on Windows: Lessons Learned]]></summary></entry><entry><title type="html">Oracle Linux STIG Automation for Air-Gapped Security Operations</title><link href="https://kimcheegi.github.io/2026/07/26/oracle-linux-stig-ansible-air-gapped-security-platform.html" rel="alternate" type="text/html" title="Oracle Linux STIG Automation for Air-Gapped Security Operations" /><published>2026-07-26T00:00:00+00:00</published><updated>2026-07-26T00:00:00+00:00</updated><id>https://kimcheegi.github.io/2026/07/26/oracle-linux-stig-ansible-air-gapped-security-platform</id><content type="html" xml:base="https://kimcheegi.github.io/2026/07/26/oracle-linux-stig-ansible-air-gapped-security-platform.html"><![CDATA[<h1 id="oracle-linux-stig-automation-for-air-gapped-security-operations">Oracle Linux STIG Automation for Air-Gapped Security Operations</h1>

<p>Security and compliance leadership increasingly depends on the ability to operationalize control frameworks at scale while maintaining governance, auditability, and resilience. In environments where internet access is restricted or deliberately disconnected, this challenge becomes even more significant.</p>

<p>This is precisely where a portable Oracle Linux control appliance can provide strategic value. Designed to support STIG automation, Ansible-driven remediation, and offline Security Onion deployment workflows, it offers an enterprise-ready foundation for hardening and compliance operations in sensitive environments.</p>

<h2 id="executive-summary">Executive Summary</h2>

<p>For organizations operating in regulated, classified, or highly constrained networks, maintaining a defensible security posture requires more than manual configuration and periodic audits. It requires a repeatable operating model that can enforce baseline controls, produce evidence, and support continuous oversight without relying on external connectivity.</p>

<p>A purpose-built Oracle Linux VM addresses this need by serving as a centralized control plane for:</p>

<ul>
  <li>STIG assessment and reporting</li>
  <li>Ansible-based hardening and remediation</li>
  <li>Offline package and repository management</li>
  <li>Security Onion deployment preparation in air-gapped environments</li>
</ul>

<p>The result is a practical and scalable platform for enterprise security operations that improves compliance readiness while reducing operational risk.</p>

<h2 id="why-this-matters-for-cisos-and-compliance-teams">Why This Matters for CISOs and Compliance Teams</h2>

<p>For security leaders, the challenge is not only implementing controls, but demonstrating that those controls are effective, consistent, and auditable. This architecture supports that objective by providing a repeatable foundation for hardening, remediation, and evidence collection in environments where manual processes are often too slow or too fragile.</p>

<p>It enables compliance teams to move from periodic reporting toward a more defensible, evidence-based model of continuous assurance, with stronger alignment to regulatory, contractual, and internal governance expectations.</p>

<h2 id="cism-mapping-for-the-project">CISM Mapping for the Project</h2>

<p>This architecture aligns with the CISM framework by delivering capabilities across mission-critical domains:</p>

<ul>
  <li><strong>Governance:</strong> Structured compliance workflows, centralized evidence collection, and policy-aligned reporting support board- and executive-level oversight.</li>
  <li><strong>Risk Management:</strong> Automated STIG evaluation and Ansible-driven remediation reduce exposure and enable more timely risk decisions.</li>
  <li><strong>Program Development:</strong> Repeatable deployment workflows, baseline hardening, and offline readiness help institutionalize security controls in constrained environments.</li>
  <li><strong>Incident Management:</strong> Offline Security Onion preparation and audit-grade reporting improve the speed and quality of investigation and response planning.</li>
</ul>

<h2 id="why-this-matters-for-executive-leadership">Why This Matters for Executive Leadership</h2>

<p>Air-gapped environments are common in government, defense, critical infrastructure, and regulated industry settings. In these contexts, leaders must balance three priorities:</p>

<ul>
  <li>maintaining mission readiness</li>
  <li>reducing cyber risk</li>
  <li>demonstrating control effectiveness to auditors and stakeholders</li>
</ul>

<p>A VMware-based Oracle Linux appliance supports these goals by enabling organizations to standardize security operations, improve consistency across systems, and preserve the evidence needed for oversight and reporting.</p>

<h2 id="core-components">Core Components</h2>

<h3 id="1-oracle-linux-control-appliance">1. Oracle Linux Control Appliance</h3>
<p>The base platform provides a secure, stable, and enterprise-supported foundation for hosting automation tools and compliance workflows.</p>

<h3 id="2-ansible-automation-engine">2. Ansible Automation Engine</h3>
<p>Ansible enables organizations to implement repeatable hardening and remediation workflows across distributed systems with greater consistency and less manual effort.</p>

<h3 id="3-stig-manager-and-evaluation-workflow">3. STIG Manager and Evaluation Workflow</h3>
<p>STIG Manager provides the governance layer for tracking compliance outcomes, compiling assessment evidence, and aligning operational activity with recognized security baselines.</p>

<h3 id="4-offline-security-onion-workflow">4. Offline Security Onion Workflow</h3>
<p>In isolated environments, Security Onion deployment can be prepared and staged offline using curated repositories, templates, and configuration artifacts, enabling monitoring capabilities without external connectivity.</p>

<h2 id="reference-architecture">Reference Architecture</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+---------------------------------------------------------------+
|              Oracle Linux Control Appliance                  |
|                                                               |
|  +----------------------+   +------------------------------+ |
|  |   STIG Manager       |   |   Ansible Automation Engine | |
|  | - Reporting          |   | - Playbooks &amp; Roles        | |
|  | - Compliance Data   |   | - Remediation Workflow     | |
|  +----------+-----------+   | - Offline Repository Prep  | |
|             |               +--------------+---------------+ |
|             |                              |                 |
|             +----------- STIG / CKL / JSON Reports ---------+ |
|                                                              |
+---------------------------+----------------------------------+
                            |
                            v
+---------------------------------------------------------------+
|                 Air-Gapped Target Environment                |
|  - Oracle Linux / RHEL systems                               |
|  - Security Onion deployment workflow                        |
|  - STIG assessment and compliance reporting                 |
+---------------------------------------------------------------+
</code></pre></div></div>

<h2 id="operational-benefits">Operational Benefits</h2>

<p>This approach offers several strategic advantages for enterprise security programs:</p>

<ul>
  <li>standardized deployment of security controls</li>
  <li>faster remediation of configuration drift</li>
  <li>stronger audit readiness through structured reporting</li>
  <li>improved support for disconnected operations</li>
  <li>a reusable foundation for future compliance automation initiatives</li>
</ul>

<h2 id="a-strategic-fit-for-modern-security-programs">A Strategic Fit for Modern Security Programs</h2>

<p>The value of this architecture extends beyond technical automation. It supports a broader governance mindset by aligning infrastructure configuration, security controls, and compliance evidence within a disciplined operating model. For CISOs and compliance leaders, that means stronger oversight, better defensibility, and more reliable reporting in environments where manual assurance is insufficient.</p>

<h2 id="conclusion">Conclusion</h2>

<p><strong>CISM Alignment:</strong> This platform supports governance, risk management, program development, and incident management by providing a repeatable, audit-ready automation foundation for air-gapped operations.</p>

<p>For organizations operating in high-security, disconnected, or regulated environments, a portable Oracle Linux appliance for Ansible and STIG automation provides a compelling foundation. It enables teams to strengthen hardening practices, improve compliance reporting, and support secure monitoring in air-gapped networks without compromising control, traceability, or operational resilience.</p>

<p>As enterprise security programs continue to mature, architectures like this will play a growing role in connecting automation with assurance.</p>]]></content><author><name>Charles</name></author><category term="oracle-linux" /><category term="ansible" /><category term="stig" /><category term="security" /><category term="compliance" /><category term="devsecops" /><category term="air-gapped" /><category term="securityonion" /><summary type="html"><![CDATA[Oracle Linux STIG Automation for Air-Gapped Security Operations]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kimcheegi.github.io/assets/images/air-gapped-stig-banner.svg" /><media:content medium="image" url="https://kimcheegi.github.io/assets/images/air-gapped-stig-banner.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A Technical Deep Dive into Oracle Linux STIG and Ansible Automation</title><link href="https://kimcheegi.github.io/2026/07/26/oracle-linux-stig-ansible-automation-deep-dive.html" rel="alternate" type="text/html" title="A Technical Deep Dive into Oracle Linux STIG and Ansible Automation" /><published>2026-07-26T00:00:00+00:00</published><updated>2026-07-26T00:00:00+00:00</updated><id>https://kimcheegi.github.io/2026/07/26/oracle-linux-stig-ansible-automation-deep-dive</id><content type="html" xml:base="https://kimcheegi.github.io/2026/07/26/oracle-linux-stig-ansible-automation-deep-dive.html"><![CDATA[<h1 id="a-technical-deep-dive-into-oracle-linux-stig-and-ansible-automation">A Technical Deep Dive into Oracle Linux STIG and Ansible Automation</h1>

<p>For organizations operating in air-gapped or restricted networks, security automation must be both practical and resilient. The most effective solutions are not only capable of applying controls, but also of preserving evidence, reducing drift, and supporting repeatable deployment across distributed systems.</p>

<p>This post outlines a technical approach for building an Oracle Linux-based compliance automation workflow using Ansible, STIG-aligned content, and offline repository strategies.</p>

<h2 id="architectural-approach">Architectural Approach</h2>

<p>The workflow can be organized into four layers:</p>

<ol>
  <li>Infrastructure layer
    <ul>
      <li>Oracle Linux or RHEL-compatible hosts</li>
      <li>VMware-based deployment model for portability</li>
      <li>local or mirrored package repositories</li>
    </ul>
  </li>
  <li>Automation layer
    <ul>
      <li>Ansible playbooks and roles</li>
      <li>inventory-driven execution</li>
      <li>role-based hardening logic</li>
    </ul>
  </li>
  <li>Compliance layer
    <ul>
      <li>STIG evaluation and reporting</li>
      <li>evidence collection in JSON, CKL, or similar formats</li>
      <li>centralized tracking of results</li>
    </ul>
  </li>
  <li>Operations layer
    <ul>
      <li>offline package staging</li>
      <li>deployment preparation for Security Onion or related monitoring services</li>
      <li>audit-ready reporting and change tracking</li>
    </ul>
  </li>
</ol>

<h2 id="why-ansible-fits-this-model">Why Ansible Fits This Model</h2>

<p>Ansible is well suited to this environment because it provides a simple, agentless model for enforcing configuration baselines across many systems. In practice, that means teams can:</p>

<ul>
  <li>define a standard hardening baseline once</li>
  <li>reuse playbooks across multiple systems</li>
  <li>apply changes consistently with version control</li>
  <li>integrate with existing operational and governance processes</li>
</ul>

<p>For disconnected networks, this becomes especially valuable because the automation logic can be staged locally and executed without relying on external services.</p>

<h2 id="recommended-workflow">Recommended Workflow</h2>

<p>A typical deployment pattern would include:</p>

<ul>
  <li>preparing offline repositories and dependency bundles</li>
  <li>staging Ansible roles and collections locally</li>
  <li>defining host groups and inventory variables</li>
  <li>applying STIG-aligned playbooks to target systems</li>
  <li>collecting compliance results for reporting and review</li>
</ul>

<p>This approach allows security teams to build a compliance workflow that is both repeatable and operationally manageable.</p>

<h2 id="operational-considerations">Operational Considerations</h2>

<p>Successful implementation depends on several practical controls:</p>

<ul>
  <li>versioned playbooks and role definitions</li>
  <li>clear inventory segmentation by environment or mission scope</li>
  <li>documented rollback procedures</li>
  <li>evidence retention for audit and reporting purposes</li>
  <li>controlled package mirror strategies for disconnected use cases</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>An Oracle Linux STIG automation workflow built around Ansible is a strong fit for organizations that need to balance security, compliance, and operational efficiency in restricted environments. The combination of automation, evidence collection, and offline readiness creates a foundation that is both practical and defensible.</p>]]></content><author><name>Charles</name></author><category term="oracle-linux" /><category term="ansible" /><category term="stig" /><category term="automation" /><category term="compliance" /><category term="devsecops" /><summary type="html"><![CDATA[A Technical Deep Dive into Oracle Linux STIG and Ansible Automation]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://kimcheegi.github.io/assets/images/air-gapped-stig-banner.svg" /><media:content medium="image" url="https://kimcheegi.github.io/assets/images/air-gapped-stig-banner.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">MCP and Playwright Auto Stack for Cyber</title><link href="https://kimcheegi.github.io/2026/05/31/MCP-and-Playwright-Auto-Stack-for-Cyber.html" rel="alternate" type="text/html" title="MCP and Playwright Auto Stack for Cyber" /><published>2026-05-31T00:00:00+00:00</published><updated>2026-05-31T00:00:00+00:00</updated><id>https://kimcheegi.github.io/2026/05/31/MCP-and-Playwright-Auto-Stack-for-Cyber</id><content type="html" xml:base="https://kimcheegi.github.io/2026/05/31/MCP-and-Playwright-Auto-Stack-for-Cyber.html"><![CDATA[<h1 id="mcp--playwright-a-modern-automation-stack-for-cybersecurity">MCP + Playwright: A Modern Automation Stack for Cybersecurity</h1>
<h3 id="a-practical-governancealigned-approach-for-cisos-and-security-engineers">A practical, governance‑aligned approach for CISOs and security engineers</h3>

<h2 id="executive-summary">Executive Summary</h2>

<p>Modern cyber threats increasingly rely on <strong>browser‑based attack vectors</strong>—phishing kits, malicious JavaScript, dynamic redirects, and client‑side payload delivery. Traditional scanners often miss these behaviors because they lack real execution context.</p>

<p>To address this gap, the combination of <strong>Model Context Protocol (MCP)</strong> and <strong>Playwright</strong> provides a scalable, repeatable, and evidence‑driven automation stack for cybersecurity teams. This article explores how MCP + Playwright enables dynamic analysis, strengthens governance, and supports CISM‑aligned security operations.</p>

<hr />

<h2 id="why-browser-automation-matters-now">Why Browser Automation Matters Now</h2>

<p>Attackers have shifted toward <strong>client‑side logic</strong>, making the browser itself a critical point of analysis. Examples include:</p>

<ul>
  <li>MFA‑aware phishing kits</li>
  <li>Obfuscated JavaScript loaders</li>
  <li>Conditional redirects based on geolocation or user agent</li>
  <li>Credential harvesting portals</li>
  <li>Malicious third‑party integrations</li>
</ul>

<p>Static scanners cannot reliably detect these behaviors.<br />
Browser automation <strong>executes the page exactly as a user would</strong>, revealing the real threat surface.</p>

<hr />

<h2 id="introducing-mcp--playwright">Introducing MCP + Playwright</h2>

<h3 id="what-is-mcp">What is MCP?</h3>

<p><strong>Model Context Protocol (MCP)</strong> is an emerging standard that allows tools and automation engines to communicate through a structured, secure interface. It provides:</p>

<ul>
  <li>Controlled execution</li>
  <li>Auditability</li>
  <li>Extensibility</li>
  <li>Governance alignment</li>
</ul>

<h3 id="what-is-playwright">What is Playwright?</h3>

<p><strong>Playwright</strong> is a browser automation framework capable of:</p>

<ul>
  <li>Launching Chromium, Firefox, WebKit</li>
  <li>Capturing screenshots</li>
  <li>Monitoring network traffic</li>
  <li>Extracting scripts</li>
  <li>Dumping DOM content</li>
  <li>Executing JavaScript in context</li>
</ul>

<p>Together, MCP orchestrates workflows while Playwright performs real browser execution.</p>

<hr />

<h2 id="architecture-overview">Architecture Overview</h2>
<p>+—————————+
|     VS Code / Agent       |
|  (MCP Client Interface)   |
+————-+————-+
|
| MCP Requests
v
+—————————+
|     MCP Playwright Server |
|  - Screenshot Tool        |
|  - Script Extractor       |
|  - Network Monitor        |
|  - DOM Dump               |
+————-+————-+
|
| Browser Automation
v
+—————————+
|       Playwright          |
|  Headless Browser Engine  |
+—————————+
—</p>

<h2 id="key-cybersecurity-workflows">Key Cybersecurity Workflows</h2>

<h3 id="1-phishing-analysis">1. Phishing Analysis</h3>
<p>Run phishing-analysis on https://suspicious-domain.com</p>

<p>Outputs:</p>

<ul>
  <li>Screenshot of the page</li>
  <li>External script inventory</li>
  <li>Network request log</li>
  <li>Indicators of compromise (IOCs)</li>
</ul>

<h3 id="2-reconnaissance">2. Reconnaissance</h3>
<p>Run recon on https://target-site.com</p>

<p>Outputs:</p>

<ul>
  <li>C2 callbacks</li>
  <li>Suspicious third‑party domains</li>
  <li>Payload delivery attempts</li>
</ul>

<hr />

<h2 id="governance--ciso-alignment">Governance &amp; CISO Alignment</h2>

<p>This automation stack supports CISM domains:</p>

<h3 id="domain-1--governance"><strong>Domain 1 — Governance</strong></h3>
<ul>
  <li>Structured, auditable workflows</li>
  <li>Clear separation of duties</li>
  <li>Policy‑aligned automation boundaries</li>
</ul>

<h3 id="domain-2--risk-management"><strong>Domain 2 — Risk Management</strong></h3>
<ul>
  <li>Evidence‑based threat validation</li>
  <li>Attack surface mapping</li>
  <li>Real‑context analysis for risk scoring</li>
</ul>

<h3 id="domain-3--security-program-development"><strong>Domain 3 — Security Program Development</strong></h3>
<ul>
  <li>Repeatable workflows</li>
  <li>Continuous validation of controls</li>
  <li>Integration with SIEM/TI platforms</li>
</ul>

<h3 id="domain-4--incident-management"><strong>Domain 4 — Incident Management</strong></h3>
<ul>
  <li>Faster triage</li>
  <li>Automated phishing investigation</li>
  <li>IOC extraction and enrichment</li>
</ul>

<hr />

<h2 id="example-output-network-monitor">Example Output (Network Monitor)</h2>

<p>```json
{
  “url”: “https://suspicious-domain.com/login”,
  “requests”: [
    { “method”: “GET”, “url”: “https://cdn.badjs.io/loader.js” },
    { “method”: “POST”, “url”: “https://exfil.attacker.net/collect” }
  ]
}</p>]]></content><author><name>Charles</name></author><category term="cybersecurity" /><category term="automation" /><category term="MCP" /><category term="Playwright" /><category term="CISO" /><category term="ISACA" /><category term="threat-analysis" /><summary type="html"><![CDATA[MCP + Playwright: A Modern Automation Stack for Cybersecurity A practical, governance‑aligned approach for CISOs and security engineers]]></summary></entry></feed>