Skip to content

V2r4

V2r4 #11

Workflow file for this run

name: Ubuntu 20 LXD VM Testing Matrix
on:
pull_request:
push:
branches:
- main
jobs:
validate:
name: Validate my profile
runs-on: ubuntu-latest
env:
CHEF_LICENSE: accept-silent
CHEF_LICENSE_KEY: ${{ secrets.SAF_CHEF_LICENSE_KEY }}
KITCHEN_LOCAL_YAML: kitchen.lxd.yml
PLATFORM: "ubuntu-20"
LC_ALL: "en_US.UTF-8"
LXD_IMAGE: "ubuntu:20.04"
LXD_VM_USERNAME: "ubuntu"
strategy:
matrix:
suite: ["vanilla", "hardened"]
fail-fast: false
steps:
- name: Install host packages
run: sudo apt-get update && sudo apt-get -y install jq
- name: Install LXD
run: |
sudo snap install lxd
# Wait for LXD daemon to start
echo "Waiting for LXD daemon to start..."
sleep 15
# Initialize LXD with default settings
sudo lxd init --auto
# Add runner to lxd group
sudo usermod -a -G lxd runner
# Fix socket permissions for CI environment
sudo chmod 666 /var/snap/lxd/common/lxd/unix.socket
echo "Configuring LXD networking (lxdbr0 with IPv4 NAT, IPv6 disabled)"
# Ensure managed bridge exists and is configured for IPv4 NAT and DNS
sudo lxc network show lxdbr0 || true
sudo lxc network set lxdbr0 ipv4.address auto
sudo lxc network set lxdbr0 ipv4.nat true
sudo lxc network set lxdbr0 ipv6.address none || true
sudo lxc network set lxdbr0 ipv6.nat false || true
sudo lxc network set lxdbr0 dns.mode managed || true
sudo lxc network set lxdbr0 dns.domain lxd || true
sudo lxc network set lxdbr0 dns.nameservers 1.1.1.1,8.8.8.8 || true
# Make sure default profile attaches NIC to lxdbr0
sudo lxc profile device add default eth0 nic name=eth0 network=lxdbr0 || true
# Helpful sysctl in some CI hosts (usually already enabled by LXD rules)
sudo sysctl -w net.ipv4.ip_forward=1 || true
sudo sysctl -w net.ipv4.conf.all.forwarding=1 net.ipv4.conf.default.forwarding=1 || true
# Ensure NAT and forwarding rules for lxdbr0 on CI hosts with restrictive FORWARD policies
EXTIF=$(ip -4 route show default | awk '{print $5}' | head -n1)
SUBNET=$(ip -4 addr show dev lxdbr0 | awk '/inet /{print $2}')
echo "Ensuring iptables NAT and FORWARD rules for $SUBNET -> $EXTIF"
sudo iptables -t nat -C POSTROUTING -s "$SUBNET" -o "$EXTIF" -j MASQUERADE 2>/dev/null || sudo iptables -t nat -A POSTROUTING -s "$SUBNET" -o "$EXTIF" -j MASQUERADE
sudo iptables -C FORWARD -i lxdbr0 -o "$EXTIF" -j ACCEPT 2>/dev/null || sudo iptables -I FORWARD 1 -i lxdbr0 -o "$EXTIF" -j ACCEPT
sudo iptables -C FORWARD -i "$EXTIF" -o lxdbr0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || sudo iptables -I FORWARD 1 -i "$EXTIF" -o lxdbr0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Some hosts default to DROP on FORWARD; set ACCEPT and enable bridge nf hooks
sudo modprobe br_netfilter || true
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1 net.bridge.bridge-nf-call-ip6tables=1 || true
sudo iptables -P FORWARD ACCEPT || true
# Relax reverse path filtering that can drop forwarded replies in some clouds
sudo sysctl -w net.ipv4.conf.all.rp_filter=0 net.ipv4.conf.default.rp_filter=0 || true
sudo sysctl -w net.ipv4.conf.lxdbr0.rp_filter=0 || true
sudo sysctl -w net.ipv4.conf.$EXTIF.rp_filter=0 || true
# Test basic LXD functionality
sudo lxc list
- name: Check out repository
uses: actions/checkout@v4
- name: Clone full repository so we can push
run: git fetch --prune --unshallow
- name: Set short git commit SHA
id: vars
run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Confirm git commit SHA output
run: echo ${{ env.COMMIT_SHORT_SHA }}
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.1"
- name: Disable ri and rdoc
run: 'echo "gem: --no-ri --no-rdoc" >> ~/.gemrc'
- name: Run Bundle Install
run: bundle install
- name: Installed Cinc-auditor
run: bundle exec cinc-auditor version
- name: Vendor the Profile
run: bundle exec cinc-auditor vendor . --overwrite
- name: Generate ephemeral SSH key for Kitchen
run: |
set -euo pipefail
KEY="$RUNNER_TEMP/kitchen_ed25519"
ssh-keygen -t ed25519 -a 64 -N "" -f "$KEY" -C "kitchen-ci-$(date -u +%Y%m%dT%H%M%SZ)" </dev/null
chmod 600 "$KEY"
chmod 644 "$KEY.pub"
{
echo "KITCHEN_SSH_KEY=$KEY"
echo "KITCHEN_SSH_PUBKEY=$(cat "$KEY.pub")"
} >> "$GITHUB_ENV"
- name: Launch LXD VM with cloud-init enabling SSH
run: |
set -euo pipefail
NAME="${{ matrix.suite }}-${{ env.PLATFORM }}"
cat > user-data.yaml <<EOF
#cloud-config
write_files:
- path: /etc/apt/apt.conf.d/99force-ipv4
permissions: '0644'
content: |
Acquire::ForceIPv4 "true";
package_update: true
packages:
- openssh-server
users:
- name: ${LXD_VM_USERNAME}
groups: [sudo]
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ${KITCHEN_SSH_PUBKEY}
ssh_pwauth: false
EOF
sudo lxc launch ${LXD_IMAGE} "$NAME" --vm -c user.user-data="$(cat user-data.yaml)"
- name: Wait for VM to be ready
run: |
echo "Waiting for VM to be fully initialized..."
# Debug: Check VM status
echo "=== VM Status ==="
NAME="${{ matrix.suite }}-${{ env.PLATFORM }}"
sudo lxc list
sudo lxc info $NAME
# We check for basic readiness instead of waiting for cloud-init completion
timeout=300
elapsed=0
while [ $elapsed -lt $timeout ]; do
# Debug: Show what we're checking
echo "=== Checking VM readiness (attempt $((elapsed/10 + 1))) ==="
# Check if VM is running and we can execute commands
if sudo lxc list --format=csv | grep -q "$NAME.*RUNNING"; then
echo "VM is in RUNNING state"
# Try to execute a simple command to verify VM is responsive
if sudo lxc exec $NAME -- echo "VM is responsive" 2>/dev/null; then
echo "VM is responsive to commands"
# Check if basic system is ready (systemd, if available)
if sudo lxc exec $NAME -- systemctl is-system-running --wait 2>/dev/null || true; then
echo "VM system is ready!"
break
else
echo "VM system not fully ready yet, but responsive"
# For basic VMs, being responsive might be enough
if [ $elapsed -ge 60 ]; then # After 60 seconds, if responsive, consider ready
echo "VM has been responsive for sufficient time, considering ready"
break
fi
fi
else
echo "VM not responsive to commands yet"
fi
else
echo "VM not in RUNNING state yet"
fi
echo "Waiting for VM to be ready... ($elapsed/$timeout seconds)"
sleep 10
elapsed=$((elapsed + 10))
done
if [ $elapsed -ge $timeout ]; then
echo "=== TIMEOUT DEBUGGING ==="
echo "Final VM status:"
sudo lxc list
sudo lxc info $NAME
echo "Timeout waiting for VM to be ready"
exit 1
fi
echo "VM is ready for use!"
- name: Export VM IP for Kitchen (KITCHEN_HOST)
run: |
set -euo pipefail
NAME="${{ matrix.suite }}-${{ env.PLATFORM }}"
# Wait up to 120s for an IP address to appear and scan all interfaces, not just eth0
timeout=120
interval=5
ipv4=""
ipv6=""
elapsed=0
while [ $elapsed -lt $timeout ]; do
ipv4=$(sudo lxc list "$NAME" --format=json | jq -r '.[0].state.network | to_entries[] | .value.addresses[]? | select(.family=="inet" and .scope=="global") | .address' | head -n1 || true)
ipv6=$(sudo lxc list "$NAME" --format=json | jq -r '.[0].state.network | to_entries[] | .value.addresses[]? | select(.family=="inet6" and .scope=="global") | .address' | head -n1 || true)
if [ -n "$ipv4" ] || [ -n "$ipv6" ]; then
break
fi
echo "Waiting for VM IP... ($elapsed/$timeout seconds)"
sleep $interval
elapsed=$((elapsed + interval))
done
# Fallback: query IPs from inside the VM if LXD hasn't reported them yet
if [ -z "$ipv4" ] && [ -z "$ipv6" ]; then
echo "Falling back to querying IPs from inside the VM..."
set +e
ipv4=$(sudo lxc exec "$NAME" -- bash -lc "ip -o -4 addr show scope global | awk '{print \\4}' | cut -d/ -f1 | head -n1" 2>/dev/null)
ipv6=$(sudo lxc exec "$NAME" -- bash -lc "ip -o -6 addr show scope global | awk '{print \\4}' | cut -d/ -f1 | head -n1" 2>/dev/null)
set -e
fi
if [ -n "$ipv4" ]; then
echo "Using IPv4 $ipv4 for KITCHEN_HOST"
echo "KITCHEN_HOST=$ipv4" >> "$GITHUB_ENV"
elif [ -n "$ipv6" ]; then
echo "Using IPv6 $ipv6 for KITCHEN_HOST"
echo "KITCHEN_HOST=$ipv6" >> "$GITHUB_ENV"
else
echo "Failed to discover a VM IP address"
sudo lxc list "$NAME"
sudo lxc info "$NAME" || true
exit 1
fi
- name: Run kitchen test
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
continue-on-error: true
run: bundle exec kitchen test --destroy=always ${{ matrix.suite }}-${{ env.PLATFORM }}
- name: Save our ${{ matrix.suite }} results summary
continue-on-error: true
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
uses: mitre/saf_action@v1.5.2
with:
command_string: "view summary -j -i spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json -o spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-data.json"
- name: Save Test Result JSON
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLATFORM }}_${{ matrix.suite }}.json
path: spec/results/
- name: Upload ${{ matrix.suite }} to Heimdall
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
continue-on-error: true
run: |
curl -# -s -F data=@spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json -F "filename=${{ env.PLATFORM }}_${{ matrix.suite }}-${{ env.COMMIT_SHORT_SHA }}.json" -F "public=true" -F "evaluationTags=${{ env.COMMIT_SHORT_SHA }},${{ github.repository }},${{ github.workflow }}" -H "Authorization: Api-Key ${{ secrets.SAF_HEIMDALL_UPLOAD_KEY }}" "${{ vars.SAF_HEIMDALL_URL }}/evaluations"
- name: Display our ${{ matrix.suite }} results summary
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
uses: mitre/saf_action@v1.5.2
with:
command_string: "view summary -i spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json"
- name: Generate Markdown Summary
continue-on-error: true
id: generate-summary
run: |
cat spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-data.json | python markdown-summary.py > spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-markdown-summary.md
cat spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-markdown-summary.md >> $GITHUB_STEP_SUMMARY
- name: Ensure the scan meets our ${{ matrix.suite }} results threshold
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
uses: mitre/saf_action@v1.5.2
with:
command_string: "validate threshold -i spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json -F ${{ matrix.suite }}.threshold.yml"
- name: Cleanup ephemeral SSH key
if: always()
run: |
rm -f "$RUNNER_TEMP/kitchen_ed25519" "$RUNNER_TEMP/kitchen_ed25519.pub"