Skip to content

Commit 7edefa9

Browse files
committed
Add daily egress connectivity allow list generation
Introduces an optional egress capture capability that builds and maintains docs/EGRESS_ALLOWLIST.yaml — a list of outbound connections (DNS name, protocol, port) made by the Landing Zone VM during a full deployment. Normal CI runs are completely unaffected. How it works: OpenSnitch runs on the Landing Zone in accept-all + log mode. After deployment, logs are collected, RFC1918 and loopback destinations are filtered out, and the remainder is rendered as a sorted YAML allow list. The daily egress-capture workflow calls the existing e2e-deployment workflow for both connected and disconnected modes with capture enabled, then diffs the result against the current allow list. If anything changed, it opens a PR automatically. For iterating on the capture tooling itself without waiting for a full e2e run, /test capture-infra triggers the same pipeline through infra-verify. To preview allow list changes on a PR branch, /test capture-egress runs a dry-run and posts the diff as a comment. https://redhat.atlassian.net/browse/MGMT-23743 Signed-off-by: Rafa Porres Molina <rporresm@redhat.com> Assisted-by: Claude Sonnet 4.6
1 parent ed0c8bb commit 7edefa9

15 files changed

Lines changed: 840 additions & 7 deletions

.github/TRIGGER_CAPTURE_E2E

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# TEMPORARY — delete before merging

.github/TRIGGER_CAPTURE_INFRA

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fri Apr 24 12:26:27 CEST 2026

.github/workflows/e2e-deployment.yml

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ name: E2E Deployment
1010
# selecting which modes to run, skipping cleanup, and sending Slack notifications.
1111

1212
on:
13+
workflow_call:
14+
inputs:
15+
run-connected:
16+
type: boolean
17+
default: true
18+
run-disconnected:
19+
type: boolean
20+
default: true
21+
storage-plugin:
22+
type: string
23+
default: 'lvms'
24+
skip-cleanup:
25+
type: boolean
26+
default: false
27+
capture-egress:
28+
type: boolean
29+
default: false
30+
outputs:
31+
egress-artifact-connected:
32+
description: "Artifact name for connected egress logs (empty if not captured)"
33+
value: ${{ jobs.e2e-connected.outputs.egress-artifact }}
34+
egress-artifact-disconnected:
35+
description: "Artifact name for disconnected egress logs (empty if not captured)"
36+
value: ${{ jobs.e2e-disconnected.outputs.egress-artifact }}
1337
schedule:
1438
# Run daily at 10:00 PM EST (03:00 UTC)
1539
- cron: '0 3 * * *'
@@ -113,6 +137,16 @@ jobs:
113137
echo "storage_plugins_connected=[\"${PLUGIN}\"]" >> $GITHUB_OUTPUT
114138
fi
115139
echo "Manual trigger - E2E will run (${PLUGIN})" | tee -a $GITHUB_STEP_SUMMARY
140+
elif [[ "${{ github.event_name }}" == "workflow_call" ]]; then
141+
echo "should_run=true" >> $GITHUB_OUTPUT
142+
PLUGIN="${{ inputs.storage-plugin || 'lvms' }}"
143+
echo "storage_plugins=[\"${PLUGIN}\"]" >> $GITHUB_OUTPUT
144+
if [[ "$PLUGIN" == "odf" ]]; then
145+
echo "storage_plugins_connected=[\"lvms\"]" >> $GITHUB_OUTPUT
146+
else
147+
echo "storage_plugins_connected=[\"${PLUGIN}\"]" >> $GITHUB_OUTPUT
148+
fi
149+
echo "Called as reusable workflow — E2E will run (${PLUGIN})" | tee -a $GITHUB_STEP_SUMMARY
116150
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
117151
echo "should_run=true" >> $GITHUB_OUTPUT
118152
echo "storage_plugins=[\"lvms\"]" >> $GITHUB_OUTPUT
@@ -149,9 +183,12 @@ jobs:
149183
needs: check-e2e-needed
150184
if: >-
151185
needs.check-e2e-needed.outputs.should_run == 'true' &&
152-
(github.event_name != 'workflow_dispatch' || inputs.run-connected == true)
186+
(github.event_name != 'workflow_dispatch' || inputs.run-connected == true) &&
187+
(github.event_name != 'workflow_call' || inputs.run-connected != false)
153188
runs-on: [self-hosted, enclave-large]
154189
timeout-minutes: 210
190+
outputs:
191+
egress-artifact: ${{ steps.egress_upload.outputs.artifact-id != '' && format('egress-logs-connected-{0}-{1}', env.ENCLAVE_CLUSTER_NAME, github.run_id) || '' }}
155192

156193
strategy:
157194
fail-fast: false
@@ -238,6 +275,13 @@ jobs:
238275
make -f Makefile.ci provision-landing-zone
239276
echo "Landing Zone provisioned" >> $GITHUB_STEP_SUMMARY
240277
278+
- name: Setup OpenSnitch for egress capture
279+
if: inputs.capture-egress == true
280+
run: |
281+
echo "## Setting up OpenSnitch" >> $GITHUB_STEP_SUMMARY
282+
make -f Makefile.ci setup-opensnitch
283+
echo "OpenSnitch running on Landing Zone" >> $GITHUB_STEP_SUMMARY
284+
241285
- name: Install Enclave Lab
242286
run: |
243287
echo "## Installing Enclave Lab" >> $GITHUB_STEP_SUMMARY
@@ -336,6 +380,20 @@ jobs:
336380
id: verify_cluster
337381
run: make -f Makefile.ci verify-cluster
338382

383+
- name: Collect egress logs
384+
if: inputs.capture-egress == true && !cancelled()
385+
run: make -f Makefile.ci collect-egress-logs
386+
387+
- name: Upload egress logs
388+
if: inputs.capture-egress == true && !cancelled()
389+
id: egress_upload
390+
uses: actions/upload-artifact@v4
391+
with:
392+
name: egress-logs-connected-${{ env.ENCLAVE_CLUSTER_NAME }}-${{ github.run_id }}
393+
path: artifacts/egress/
394+
retention-days: 7
395+
if-no-files-found: warn
396+
339397
- name: Collect artifacts
340398
if: always()
341399
uses: ./.github/actions/collect-artifacts
@@ -413,7 +471,10 @@ jobs:
413471
GH_TOKEN: ${{ github.token }}
414472

415473
- name: Cleanup infrastructure
416-
if: always() && (github.event_name != 'workflow_dispatch' || inputs.skip-cleanup != true)
474+
if: >-
475+
always() &&
476+
!((github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') &&
477+
inputs.skip-cleanup == true)
417478
run: |
418479
echo "## Cleanup Infrastructure" >> $GITHUB_STEP_SUMMARY
419480
echo "" >> $GITHUB_STEP_SUMMARY
@@ -440,7 +501,10 @@ jobs:
440501
fi
441502
442503
- name: Cleanup skipped notice
443-
if: always() && github.event_name == 'workflow_dispatch' && inputs.skip-cleanup == true
504+
if: >-
505+
always() &&
506+
(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') &&
507+
inputs.skip-cleanup == true
444508
run: |
445509
echo "## Cleanup Skipped" >> $GITHUB_STEP_SUMMARY
446510
echo "" >> $GITHUB_STEP_SUMMARY
@@ -562,9 +626,12 @@ jobs:
562626
needs: check-e2e-needed
563627
if: >-
564628
needs.check-e2e-needed.outputs.should_run == 'true' &&
565-
(github.event_name != 'workflow_dispatch' || inputs.run-disconnected == true)
629+
(github.event_name != 'workflow_dispatch' || inputs.run-disconnected == true) &&
630+
(github.event_name != 'workflow_call' || inputs.run-disconnected != false)
566631
runs-on: ${{ matrix.storage-plugin == 'odf' && fromJSON('["self-hosted", "enclave-large", "odf"]') || fromJSON('["self-hosted", "enclave-large"]') }}
567632
timeout-minutes: ${{ github.event_name == 'schedule' && 600 || 360 }}
633+
outputs:
634+
egress-artifact: ${{ steps.egress_upload.outputs.artifact-id != '' && format('egress-logs-disconnected-{0}-{1}', env.ENCLAVE_CLUSTER_NAME, github.run_id) || '' }}
568635

569636
strategy:
570637
fail-fast: false
@@ -651,6 +718,13 @@ jobs:
651718
make -f Makefile.ci provision-landing-zone
652719
echo "Landing Zone provisioned" >> $GITHUB_STEP_SUMMARY
653720
721+
- name: Setup OpenSnitch for egress capture
722+
if: inputs.capture-egress == true
723+
run: |
724+
echo "## Setting up OpenSnitch" >> $GITHUB_STEP_SUMMARY
725+
make -f Makefile.ci setup-opensnitch
726+
echo "OpenSnitch running on Landing Zone" >> $GITHUB_STEP_SUMMARY
727+
654728
- name: Install Enclave Lab
655729
run: |
656730
echo "## Installing Enclave Lab" >> $GITHUB_STEP_SUMMARY
@@ -758,6 +832,20 @@ jobs:
758832
echo "### Mirror Registry Status" >> $GITHUB_STEP_SUMMARY
759833
ssh $SSH_OPTS cloud-user@$LZ_IP "podman ps --filter name=quay --format 'table {{.Names}}\t{{.Status}}'" >> $GITHUB_STEP_SUMMARY || true
760834
835+
- name: Collect egress logs
836+
if: inputs.capture-egress == true && !cancelled()
837+
run: make -f Makefile.ci collect-egress-logs
838+
839+
- name: Upload egress logs
840+
if: inputs.capture-egress == true && !cancelled()
841+
id: egress_upload
842+
uses: actions/upload-artifact@v4
843+
with:
844+
name: egress-logs-disconnected-${{ env.ENCLAVE_CLUSTER_NAME }}-${{ github.run_id }}
845+
path: artifacts/egress/
846+
retention-days: 7
847+
if-no-files-found: warn
848+
761849
- name: Collect artifacts
762850
if: always()
763851
uses: ./.github/actions/collect-artifacts
@@ -835,7 +923,10 @@ jobs:
835923
GH_TOKEN: ${{ github.token }}
836924

837925
- name: Cleanup infrastructure
838-
if: always() && (github.event_name != 'workflow_dispatch' || inputs.skip-cleanup != true)
926+
if: >-
927+
always() &&
928+
!((github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') &&
929+
inputs.skip-cleanup == true)
839930
run: |
840931
echo "## Cleanup Infrastructure" >> $GITHUB_STEP_SUMMARY
841932
echo "" >> $GITHUB_STEP_SUMMARY
@@ -862,7 +953,10 @@ jobs:
862953
fi
863954
864955
- name: Cleanup skipped notice
865-
if: always() && github.event_name == 'workflow_dispatch' && inputs.skip-cleanup == true
956+
if: >-
957+
always() &&
958+
(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') &&
959+
inputs.skip-cleanup == true
866960
run: |
867961
echo "## Cleanup Skipped" >> $GITHUB_STEP_SUMMARY
868962
echo "" >> $GITHUB_STEP_SUMMARY
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: Egress Capture
2+
3+
# Runs the full e2e deployment for both connected and disconnected modes with
4+
# OpenSnitch enabled, analyzes captured traffic, and either opens a PR to update
5+
# docs/EGRESS_ALLOWLIST.yaml (daily schedule) or posts a diff (dry-run / slash command).
6+
7+
on:
8+
schedule:
9+
# Run daily at 1:00 AM EST (06:00 UTC), after the nightly e2e (03:00 UTC)
10+
- cron: '0 6 * * *'
11+
pull_request:
12+
paths:
13+
- '.github/TRIGGER_CAPTURE_E2E'
14+
workflow_dispatch:
15+
inputs:
16+
run-connected:
17+
type: boolean
18+
default: true
19+
description: 'Capture egress for connected mode'
20+
run-disconnected:
21+
type: boolean
22+
default: true
23+
description: 'Capture egress for disconnected mode'
24+
dry-run:
25+
type: boolean
26+
default: true
27+
description: 'Show diff only; do not create a PR'
28+
pr-number:
29+
type: string
30+
default: ''
31+
description: 'PR number to comment on (set by slash command)'
32+
33+
permissions:
34+
contents: read
35+
checks: write
36+
37+
jobs:
38+
run-e2e-connected:
39+
if: github.event_name == 'schedule' || inputs.run-connected != false
40+
uses: ./.github/workflows/e2e-deployment.yml
41+
with:
42+
run-connected: true
43+
run-disconnected: false
44+
storage-plugin: lvms
45+
skip-cleanup: false
46+
capture-egress: true
47+
secrets: inherit
48+
49+
run-e2e-disconnected:
50+
if: github.event_name == 'schedule' || inputs.run-disconnected != false
51+
uses: ./.github/workflows/e2e-deployment.yml
52+
with:
53+
run-connected: false
54+
run-disconnected: true
55+
storage-plugin: lvms
56+
skip-cleanup: false
57+
capture-egress: true
58+
secrets: inherit
59+
60+
update-allowlist:
61+
name: Update egress allow list
62+
needs: [run-e2e-connected, run-e2e-disconnected]
63+
if: always() && !cancelled()
64+
runs-on: [self-hosted, pr-validation]
65+
permissions:
66+
contents: write
67+
pull-requests: write
68+
69+
steps:
70+
- name: Checkout code
71+
uses: actions/checkout@v4
72+
73+
- name: Download connected egress logs
74+
uses: actions/download-artifact@v4
75+
with:
76+
pattern: egress-logs-connected-*
77+
path: egress-logs/connected/
78+
merge-multiple: true
79+
continue-on-error: true
80+
81+
- name: Download disconnected egress logs
82+
uses: actions/download-artifact@v4
83+
with:
84+
pattern: egress-logs-disconnected-*
85+
path: egress-logs/disconnected/
86+
merge-multiple: true
87+
continue-on-error: true
88+
89+
- name: Analyze and update allow list
90+
env:
91+
DRY_RUN: ${{ github.event_name == 'schedule' && 'false' || inputs.dry-run }}
92+
PR_NUMBER: ${{ inputs.pr-number }}
93+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94+
run: scripts/egress/update_allowlist.sh

.github/workflows/infra-verify.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ name: Infrastructure Verification
22

33
on:
44
workflow_dispatch:
5+
inputs:
6+
capture-egress:
7+
type: boolean
8+
default: false
9+
description: 'Install OpenSnitch, capture egress traffic, and upload allow list YAML (smoke test)'
510
pull_request:
611
types: [opened, synchronize, reopened]
712
merge_group:
@@ -31,6 +36,17 @@ jobs:
3136
- name: Checkout code
3237
uses: actions/checkout@v4
3338

39+
# TEMPORARY: enable egress capture via trigger file (remove before merging)
40+
- name: '[TEMP] Check egress capture trigger'
41+
id: egress_trigger
42+
run: |
43+
if [ -f ".github/TRIGGER_CAPTURE_INFRA" ]; then
44+
echo "enabled=true" >> $GITHUB_OUTPUT
45+
echo "Egress capture enabled via trigger file" >> $GITHUB_STEP_SUMMARY
46+
else
47+
echo "enabled=false" >> $GITHUB_OUTPUT
48+
fi
49+
3450
- name: Generate unique cluster name
3551
id: cluster_name
3652
uses: ./.github/actions/setup-cluster-name
@@ -131,6 +147,13 @@ jobs:
131147
make -f Makefile.ci provision-landing-zone
132148
echo "✅ Landing Zone provisioned" >> $GITHUB_STEP_SUMMARY
133149
150+
- name: Setup OpenSnitch for egress capture
151+
if: inputs.capture-egress == true || steps.egress_trigger.outputs.enabled == 'true'
152+
run: |
153+
echo "## Setting up OpenSnitch" >> $GITHUB_STEP_SUMMARY
154+
make -f Makefile.ci setup-opensnitch
155+
echo "OpenSnitch running on Landing Zone" >> $GITHUB_STEP_SUMMARY
156+
134157
- name: Install Enclave Lab (Connected Mode)
135158
id: install_enclave
136159
run: |
@@ -140,6 +163,22 @@ jobs:
140163
141164
ENCLAVE_DEPLOYMENT_MODE=connected make -f Makefile.ci install-enclave 2>&1 | tee step-logs/03-install-enclave.log
142165
166+
- name: Collect egress logs
167+
if: (inputs.capture-egress == true || steps.egress_trigger.outputs.enabled == 'true') && !cancelled()
168+
run: make -f Makefile.ci collect-egress-logs
169+
170+
- name: Analyze egress and generate allow list
171+
if: (inputs.capture-egress == true || steps.egress_trigger.outputs.enabled == 'true') && !cancelled()
172+
run: |
173+
python3 scripts/egress/analyze_egress.py \
174+
artifacts/egress/opensnitch-connections.json.gz infra \
175+
> egress-allowlist-infra.yaml
176+
cat egress-allowlist-infra.yaml
177+
echo "## Egress Allow List (smoke test — infra only)" >> $GITHUB_STEP_SUMMARY
178+
echo '```yaml' >> $GITHUB_STEP_SUMMARY
179+
cat egress-allowlist-infra.yaml >> $GITHUB_STEP_SUMMARY
180+
echo '```' >> $GITHUB_STEP_SUMMARY
181+
143182
- name: Collect step logs
144183
if: always()
145184
env:
@@ -161,6 +200,8 @@ jobs:
161200
path: |
162201
ci-artifacts/
163202
step-logs/
203+
egress-allowlist-infra.yaml
204+
artifacts/egress/
164205
retention-days: 7
165206

166207
- name: Cleanup infrastructure

0 commit comments

Comments
 (0)