Skip to content

Commit 2775f4b

Browse files
committed
Create verify-lxd-vm.yml
1 parent e9e3df7 commit 2775f4b

1 file changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
name: Ubuntu 20 LXD VM Testing Matrix
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
validate:
11+
name: Validate my profile
12+
runs-on: ubuntu-latest
13+
env:
14+
CHEF_LICENSE: accept-silent
15+
CHEF_LICENSE_KEY: ${{ secrets.SAF_CHEF_LICENSE_KEY }}
16+
KITCHEN_LOCAL_YAML: kitchen.lxd.yml
17+
SAF_PIPELINE_SUBNET: ${{ secrets.SAF_PIPELINE_SUBNET }}
18+
SAF_PIPELINE_SG: ${{ secrets.SAF_PIPELINE_SG }}
19+
PLATFORM: "ubuntu-20"
20+
LC_ALL: "en_US.UTF-8"
21+
LXD_IMAGE: "ubuntu:20.04"
22+
LXD_VM_USERNAME: "ubuntu"
23+
strategy:
24+
matrix:
25+
suite: ["vanilla", "hardened"]
26+
fail-fast: false
27+
steps:
28+
- name: add needed packages
29+
run: |
30+
sudo apt-get update
31+
sudo apt-get -y install jq
32+
33+
- name: Install LXD
34+
run: |
35+
sudo snap install lxd
36+
# Wait for LXD daemon to start
37+
echo "Waiting for LXD daemon to start..."
38+
sleep 15
39+
# Initialize LXD with default settings
40+
sudo lxd init --auto
41+
# Add runner to lxd group
42+
sudo usermod -a -G lxd runner
43+
# Fix socket permissions for CI environment
44+
sudo chmod 666 /var/snap/lxd/common/lxd/unix.socket
45+
46+
echo "Configuring LXD networking (lxdbr0 with IPv4 NAT, IPv6 disabled)"
47+
# Ensure managed bridge exists and is configured for IPv4 NAT and DNS
48+
sudo lxc network show lxdbr0 || true
49+
sudo lxc network set lxdbr0 ipv4.address auto
50+
sudo lxc network set lxdbr0 ipv4.nat true
51+
sudo lxc network set lxdbr0 ipv6.address none || true
52+
sudo lxc network set lxdbr0 ipv6.nat false || true
53+
sudo lxc network set lxdbr0 dns.mode managed || true
54+
sudo lxc network set lxdbr0 dns.domain lxd || true
55+
sudo lxc network set lxdbr0 dns.nameservers 1.1.1.1,8.8.8.8 || true
56+
# Make sure default profile attaches NIC to lxdbr0
57+
sudo lxc profile device add default eth0 nic name=eth0 network=lxdbr0 || true
58+
59+
# Helpful sysctl in some CI hosts (usually already enabled by LXD rules)
60+
sudo sysctl -w net.ipv4.ip_forward=1 || true
61+
sudo sysctl -w net.ipv4.conf.all.forwarding=1 net.ipv4.conf.default.forwarding=1 || true
62+
63+
# Ensure NAT and forwarding rules for lxdbr0 on CI hosts with restrictive FORWARD policies
64+
EXTIF=$(ip -4 route show default | awk '{print $5}' | head -n1)
65+
SUBNET=$(ip -4 addr show dev lxdbr0 | awk '/inet /{print $2}')
66+
echo "Ensuring iptables NAT and FORWARD rules for $SUBNET -> $EXTIF"
67+
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
68+
sudo iptables -C FORWARD -i lxdbr0 -o "$EXTIF" -j ACCEPT 2>/dev/null || sudo iptables -I FORWARD 1 -i lxdbr0 -o "$EXTIF" -j ACCEPT
69+
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
70+
71+
# Some hosts default to DROP on FORWARD; set ACCEPT and enable bridge nf hooks
72+
sudo modprobe br_netfilter || true
73+
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1 net.bridge.bridge-nf-call-ip6tables=1 || true
74+
sudo iptables -P FORWARD ACCEPT || true
75+
76+
# Relax reverse path filtering that can drop forwarded replies in some clouds
77+
sudo sysctl -w net.ipv4.conf.all.rp_filter=0 net.ipv4.conf.default.rp_filter=0 || true
78+
sudo sysctl -w net.ipv4.conf.lxdbr0.rp_filter=0 || true
79+
sudo sysctl -w net.ipv4.conf.$EXTIF.rp_filter=0 || true
80+
81+
# Test basic LXD functionality
82+
sudo lxc list
83+
84+
- name: Check out repository
85+
uses: actions/checkout@v4
86+
87+
- name: Clone full repository so we can push
88+
run: git fetch --prune --unshallow
89+
90+
- name: Set short git commit SHA
91+
id: vars
92+
run: |
93+
calculatedSha=$(git rev-parse --short ${{ github.sha }})
94+
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
95+
96+
- name: Confirm git commit SHA output
97+
run: echo ${{ env.COMMIT_SHORT_SHA }}
98+
99+
- name: Setup Ruby
100+
uses: ruby/setup-ruby@v1
101+
with:
102+
ruby-version: "3.1"
103+
104+
- name: Disable ri and rdoc
105+
run: 'echo "gem: --no-ri --no-rdoc" >> ~/.gemrc'
106+
107+
- name: Run Bundle Install
108+
run: bundle install
109+
110+
- name: Installed Cinc-auditor
111+
run: bundle exec cinc-auditor version
112+
113+
- name: Vendor the Profile
114+
run: bundle exec cinc-auditor vendor . --overwrite
115+
116+
- name: Generate ephemeral SSH key for Kitchen
117+
run: |
118+
set -euo pipefail
119+
KEY="$RUNNER_TEMP/kitchen_ed25519"
120+
ssh-keygen -t ed25519 -a 64 -N "" -f "$KEY" -C "kitchen-ci-$(date -u +%Y%m%dT%H%M%SZ)" </dev/null
121+
chmod 600 "$KEY"
122+
chmod 644 "$KEY.pub"
123+
{
124+
echo "KITCHEN_SSH_KEY=$KEY"
125+
echo "KITCHEN_SSH_PUBKEY=$(cat "$KEY.pub")"
126+
} >> "$GITHUB_ENV"
127+
128+
- name: Launch LXD VM with cloud-init enabling SSH
129+
run: |
130+
set -euo pipefail
131+
NAME="${{ matrix.suite }}-${{ env.PLATFORM }}"
132+
133+
cat > user-data.yaml <<EOF
134+
#cloud-config
135+
write_files:
136+
- path: /etc/apt/apt.conf.d/99force-ipv4
137+
permissions: '0644'
138+
content: |
139+
Acquire::ForceIPv4 "true";
140+
package_update: true
141+
packages:
142+
- openssh-server
143+
users:
144+
- name: ${LXD_VM_USERNAME}
145+
groups: [sudo]
146+
sudo: ALL=(ALL) NOPASSWD:ALL
147+
shell: /bin/bash
148+
ssh_authorized_keys:
149+
- ${KITCHEN_SSH_PUBKEY}
150+
ssh_pwauth: false
151+
EOF
152+
153+
sudo lxc launch ${LXD_IMAGE} "$NAME" --vm -c user.user-data="$(cat user-data.yaml)"
154+
155+
- name: Wait for VM to be ready
156+
run: |
157+
echo "Waiting for VM to be fully initialized..."
158+
159+
# Debug: Check VM status
160+
echo "=== VM Status ==="
161+
NAME="${{ matrix.suite }}-${{ env.PLATFORM }}"
162+
sudo lxc list
163+
sudo lxc info $NAME
164+
165+
# We check for basic readiness instead of waiting for cloud-init completion
166+
timeout=300
167+
elapsed=0
168+
while [ $elapsed -lt $timeout ]; do
169+
# Debug: Show what we're checking
170+
echo "=== Checking VM readiness (attempt $((elapsed/10 + 1))) ==="
171+
172+
# Check if VM is running and we can execute commands
173+
if sudo lxc list --format=csv | grep -q "$NAME.*RUNNING"; then
174+
echo "VM is in RUNNING state"
175+
176+
# Try to execute a simple command to verify VM is responsive
177+
if sudo lxc exec $NAME -- echo "VM is responsive" 2>/dev/null; then
178+
echo "VM is responsive to commands"
179+
180+
# Check if basic system is ready (systemd, if available)
181+
if sudo lxc exec $NAME -- systemctl is-system-running --wait 2>/dev/null || true; then
182+
echo "VM system is ready!"
183+
break
184+
else
185+
echo "VM system not fully ready yet, but responsive"
186+
# For basic VMs, being responsive might be enough
187+
if [ $elapsed -ge 60 ]; then # After 60 seconds, if responsive, consider ready
188+
echo "VM has been responsive for sufficient time, considering ready"
189+
break
190+
fi
191+
fi
192+
else
193+
echo "VM not responsive to commands yet"
194+
fi
195+
else
196+
echo "VM not in RUNNING state yet"
197+
fi
198+
199+
echo "Waiting for VM to be ready... ($elapsed/$timeout seconds)"
200+
sleep 10
201+
elapsed=$((elapsed + 10))
202+
done
203+
204+
if [ $elapsed -ge $timeout ]; then
205+
echo "=== TIMEOUT DEBUGGING ==="
206+
echo "Final VM status:"
207+
sudo lxc list
208+
sudo lxc info $NAME
209+
echo "Timeout waiting for VM to be ready"
210+
exit 1
211+
fi
212+
213+
echo "VM is ready for use!"
214+
215+
- name: Export VM IP for Kitchen (KITCHEN_HOST)
216+
run: |
217+
set -euo pipefail
218+
NAME="${{ matrix.suite }}-${{ env.PLATFORM }}"
219+
220+
# Wait up to 120s for an IP address to appear and scan all interfaces, not just eth0
221+
timeout=120
222+
interval=5
223+
ipv4=""
224+
ipv6=""
225+
elapsed=0
226+
while [ $elapsed -lt $timeout ]; do
227+
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)
228+
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)
229+
if [ -n "$ipv4" ] || [ -n "$ipv6" ]; then
230+
break
231+
fi
232+
echo "Waiting for VM IP... ($elapsed/$timeout seconds)"
233+
sleep $interval
234+
elapsed=$((elapsed + interval))
235+
done
236+
237+
# Fallback: query IPs from inside the VM if LXD hasn't reported them yet
238+
if [ -z "$ipv4" ] && [ -z "$ipv6" ]; then
239+
echo "Falling back to querying IPs from inside the VM..."
240+
set +e
241+
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)
242+
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)
243+
set -e
244+
fi
245+
246+
if [ -n "$ipv4" ]; then
247+
echo "Using IPv4 $ipv4 for KITCHEN_HOST"
248+
echo "KITCHEN_HOST=$ipv4" >> "$GITHUB_ENV"
249+
elif [ -n "$ipv6" ]; then
250+
echo "Using IPv6 $ipv6 for KITCHEN_HOST"
251+
echo "KITCHEN_HOST=$ipv6" >> "$GITHUB_ENV"
252+
else
253+
echo "Failed to discover a VM IP address"
254+
sudo lxc list "$NAME"
255+
sudo lxc info "$NAME" || true
256+
exit 1
257+
fi
258+
259+
- name: Run kitchen test
260+
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
261+
continue-on-error: true
262+
run: bundle exec kitchen test --destroy=always ${{ matrix.suite }}-${{ env.PLATFORM }}
263+
264+
- name: Save our ${{ matrix.suite }} results summary
265+
continue-on-error: true
266+
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
267+
uses: mitre/saf_action@v1.5.2
268+
with:
269+
command_string: "view summary -j -i spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json -o spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-data.json"
270+
271+
- name: Save Test Result JSON
272+
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
273+
uses: actions/upload-artifact@v4
274+
with:
275+
name: ${{ env.PLATFORM }}_${{ matrix.suite }}.json
276+
path: spec/results/
277+
278+
- name: Upload ${{ matrix.suite }} to Heimdall
279+
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
280+
continue-on-error: true
281+
run: |
282+
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"
283+
284+
- name: Display our ${{ matrix.suite }} results summary
285+
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
286+
uses: mitre/saf_action@v1.5.2
287+
with:
288+
command_string: "view summary -i spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json"
289+
290+
- name: Generate Markdown Summary
291+
continue-on-error: true
292+
id: generate-summary
293+
run: |
294+
cat spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-data.json | python markdown-summary.py > spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-markdown-summary.md
295+
cat spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}-markdown-summary.md >> $GITHUB_STEP_SUMMARY
296+
297+
- name: Ensure the scan meets our ${{ matrix.suite }} results threshold
298+
if: ${{ !contains(steps.commit.outputs.message, 'only-validate-profile') }}
299+
uses: mitre/saf_action@v1.5.2
300+
with:
301+
command_string: "validate threshold -i spec/results/${{ env.PLATFORM }}_${{ matrix.suite }}.json -F ${{ matrix.suite }}.threshold.yml"
302+
303+
- name: Cleanup ephemeral SSH key
304+
if: always()
305+
run: |
306+
rm -f "$RUNNER_TEMP/kitchen_ed25519" "$RUNNER_TEMP/kitchen_ed25519.pub"

0 commit comments

Comments
 (0)