From a0921de0ea43a08a210a26a9061a5c9ed78283f5 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Mon, 6 Apr 2026 13:38:49 -0400 Subject: [PATCH 1/5] feat: add Gitleaks configuration and CI workflow Adds .gitleaks.toml with custom rules for Ansible-specific credential patterns (OpenShift API keys, Automation Hub tokens, container registry passwords) and allowlists for placeholder values and Jinja2 templates. Adds a GitHub Actions workflow to run Gitleaks on pushes and PRs. Resolves: MFG-376 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/gitleaks.yml | 37 +++++++++++++++++++ .gitleaks.toml | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 .github/workflows/gitleaks.yml create mode 100644 .gitleaks.toml diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..d41d323 --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,37 @@ +--- +name: Gitleaks + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + pull_request_target: + types: [opened, synchronize, reopened] + branches: ["main"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + gitleaks: + runs-on: ubuntu-latest + if: | + (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || + (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) || + (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@v2 + with: + args: --config .gitleaks.toml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..bdf4b78 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,65 @@ +# Gitleaks configuration for openshift_virtualization_migration +# https://github.com/gitleaks/gitleaks + +title = "OpenShift Virtualization Migration Gitleaks Configuration" + +[extend] +useDefault = true + +# Allowlist paths and patterns that contain placeholder credentials +# (e.g., "changeme", example domains, template variables) +[allowlist] +description = "Global allowlist for placeholder values and template files" +paths = [ + '''\.gitleaks\.toml$''', +] +regexTarget = "line" +regexes = [ + # Placeholder values used in inventory and defaults + '''changeme''', + # Jinja2 template variables + '''\{\{.*\}\}''', + # Ansible Vault references + '''!vault''', + # Example/documentation values + '''example\.com''', + '''EXAMPLE''', + # YAML comments containing credential variable names + '''^\s*#.*''', +] + +# Custom rules for Ansible-specific credential patterns +[[rules]] +id = "ansible-vault-password-file" +description = "Ansible vault password file" +regex = '''vault[_-]?pass(word)?[_-]?file\s*[:=]\s*['"]?([^\s'"]+)''' +keywords = ["vault"] +[rules.allowlist] +regexes = ['''changeme''', '''\{\{.*\}\}'''] + +[[rules]] +id = "openshift-api-key" +description = "OpenShift API key or token" +regex = '''(?i)(openshift[_-]?(?:api[_-]?key|token|password))\s*[:=]\s*['"]?([^\s'"#}{]+)''' +keywords = ["openshift"] +[rules.allowlist] +regexes = ['''changeme''', '''\{\{.*\}\}''', '''example'''] +paths = ['''defaults/main\.yml$''', '''inventory\.yml$'''] + +[[rules]] +id = "automation-hub-token" +description = "Automation Hub token" +regex = '''(?i)(automation[_-]?hub[_-]?(?:token|password))\s*[:=]\s*['"]?([^\s'"#}{]+)''' +keywords = ["automation_hub", "automation-hub"] +[rules.allowlist] +regexes = ['''changeme''', '''\{\{.*\}\}'''] +paths = ['''defaults/main\.yml$''', '''inventory\.yml$'''] + +[[rules]] +id = "container-registry-password" +description = "Container registry password" +regex = '''(?i)(container[_-]?password|registry[_-]?password)\s*[:=]\s*['"]?([^\s'"#}{]+)''' +keywords = ["container_password", "registry_password"] +[rules.allowlist] +regexes = ['''changeme''', '''\{\{.*\}\}'''] +paths = ['''defaults/main\.yml$''', '''inventory\.yml$'''] From deaa285983810be493bb7a5528d8a631fe083f13 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Tue, 14 Apr 2026 10:06:21 -0400 Subject: [PATCH 2/5] fix: resolve 11 gitleaks false positives on README and YAML files Add allowlists for docsible-generated README.md variable documentation (HTML bold tags), multi-line YAML block scalars (>- / |) where values are Jinja2 templates on the following line, and task files that reference credential variable names without containing actual secrets. Co-Authored-By: Claude Opus 4.6 --- .gitleaks.toml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index bdf4b78..972b0b5 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -12,6 +12,7 @@ useDefault = true description = "Global allowlist for placeholder values and template files" paths = [ '''\.gitleaks\.toml$''', + '''README\.md$''', ] regexTarget = "line" regexes = [ @@ -26,6 +27,10 @@ regexes = [ '''EXAMPLE''', # YAML comments containing credential variable names '''^\s*#.*''', + # HTML bold tags documenting variable names (docsible-generated) + '''.*''', + # Multi-line YAML block scalars where value is a Jinja2 template on the next line + '''[>|][+-]?\s*$''', ] # Custom rules for Ansible-specific credential patterns @@ -43,8 +48,8 @@ description = "OpenShift API key or token" regex = '''(?i)(openshift[_-]?(?:api[_-]?key|token|password))\s*[:=]\s*['"]?([^\s'"#}{]+)''' keywords = ["openshift"] [rules.allowlist] -regexes = ['''changeme''', '''\{\{.*\}\}''', '''example'''] -paths = ['''defaults/main\.yml$''', '''inventory\.yml$'''] +regexes = ['''changeme''', '''\{\{.*\}\}''', '''example''', '''.*''', '''[>|][+-]?\s*$'''] +paths = ['''defaults/main\.yml$''', '''inventory\.yml$''', '''README\.md$''', '''tasks/main\.yml$'''] [[rules]] id = "automation-hub-token" @@ -61,5 +66,5 @@ description = "Container registry password" regex = '''(?i)(container[_-]?password|registry[_-]?password)\s*[:=]\s*['"]?([^\s'"#}{]+)''' keywords = ["container_password", "registry_password"] [rules.allowlist] -regexes = ['''changeme''', '''\{\{.*\}\}'''] -paths = ['''defaults/main\.yml$''', '''inventory\.yml$'''] +regexes = ['''changeme''', '''\{\{.*\}\}''', '''.*'''] +paths = ['''defaults/main\.yml$''', '''inventory\.yml$''', '''README\.md$'''] From 2d8c044af8f02573e8f29f271ed2e8906334572c Mon Sep 17 00:00:00 2001 From: sfulmer Date: Wed, 15 Apr 2026 14:56:57 -0400 Subject: [PATCH 3/5] ci: add Slack PR notification workflow Sends alerts to Slack channel on PR open, close, merge, reopen, and review events. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/slack-pr-notifications.yml | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/slack-pr-notifications.yml diff --git a/.github/workflows/slack-pr-notifications.yml b/.github/workflows/slack-pr-notifications.yml new file mode 100644 index 0000000..8015e74 --- /dev/null +++ b/.github/workflows/slack-pr-notifications.yml @@ -0,0 +1,63 @@ +name: Slack PR Notifications + +on: + pull_request: + types: [opened, closed, reopened] + branches: ["main"] + pull_request_review: + types: [submitted] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Set notification details + id: details + run: | + if [[ "${{ github.event_name }}" == "pull_request_review" ]]; then + STATE="${{ github.event.review.state }}" + TITLE="PR Review: ${STATE} - ${{ github.event.pull_request.title }}" + COLOR=$([[ "$STATE" == "approved" ]] && echo "good" || echo "warning") + BODY="${{ github.event.review.user.login }} ${STATE} the PR" + else + ACTION="${{ github.event.action }}" + TITLE="PR ${ACTION^}: ${{ github.event.pull_request.title }}" + if [[ "$ACTION" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then + TITLE="PR Merged: ${{ github.event.pull_request.title }}" + COLOR="good" + elif [[ "$ACTION" == "opened" ]]; then + COLOR="#1a73e8" + elif [[ "$ACTION" == "reopened" ]]; then + COLOR="warning" + else + COLOR="danger" + fi + BODY="${{ github.event.pull_request.user.login }} ${ACTION} the PR" + fi + + echo "title=${TITLE}" >> $GITHUB_OUTPUT + echo "color=${COLOR}" >> $GITHUB_OUTPUT + echo "body=${BODY}" >> $GITHUB_OUTPUT + + - name: Send Slack notification + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "attachments": [ + { + "color": "${{ steps.details.outputs.color }}", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*${{ steps.details.outputs.title }}*\n${{ steps.details.outputs.body }}\n*Repo:* `${{ github.repository }}`\n*Branch:* `${{ github.event.pull_request.head.ref }}` -> `${{ github.event.pull_request.base.ref }}`\n<${{ github.event.pull_request.html_url }}|View Pull Request>" + } + } + ] + } + ] + } From 9e49f2f6a1abc7966e9e47c8a5dfc3d09eb3d061 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sun, 19 Apr 2026 12:52:06 -0400 Subject: [PATCH 4/5] fix(ci): patch shell injection and unsafe pull_request_target checkout Remove pull_request_target trigger from gitleaks workflow to prevent untrusted PR code from running with write permissions. Move GitHub context interpolation from run: blocks to env: variables in Slack notifications to prevent shell injection via crafted PR titles. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/gitleaks.yml | 8 ------ .github/workflows/slack-pr-notifications.yml | 30 ++++++++++++-------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index d41d323..2975c5a 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -6,9 +6,6 @@ on: branches: ["main"] pull_request: branches: ["main"] - pull_request_target: - types: [opened, synchronize, reopened] - branches: ["main"] workflow_dispatch: concurrency: @@ -18,15 +15,10 @@ concurrency: jobs: gitleaks: runs-on: ubuntu-latest - if: | - (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || - (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) || - (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') steps: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 - name: Run Gitleaks diff --git a/.github/workflows/slack-pr-notifications.yml b/.github/workflows/slack-pr-notifications.yml index 8015e74..d3498d3 100644 --- a/.github/workflows/slack-pr-notifications.yml +++ b/.github/workflows/slack-pr-notifications.yml @@ -13,26 +13,32 @@ jobs: steps: - name: Set notification details id: details + env: + EVENT_NAME: ${{ github.event_name }} + EVENT_ACTION: ${{ github.event.action }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_MERGED: ${{ github.event.pull_request.merged }} + PR_USER: ${{ github.event.pull_request.user.login }} + REVIEW_STATE: ${{ github.event.review.state }} + REVIEW_USER: ${{ github.event.review.user.login }} run: | - if [[ "${{ github.event_name }}" == "pull_request_review" ]]; then - STATE="${{ github.event.review.state }}" - TITLE="PR Review: ${STATE} - ${{ github.event.pull_request.title }}" - COLOR=$([[ "$STATE" == "approved" ]] && echo "good" || echo "warning") - BODY="${{ github.event.review.user.login }} ${STATE} the PR" + if [[ "$EVENT_NAME" == "pull_request_review" ]]; then + TITLE="PR Review: ${REVIEW_STATE} - ${PR_TITLE}" + COLOR=$([[ "$REVIEW_STATE" == "approved" ]] && echo "good" || echo "warning") + BODY="${REVIEW_USER} ${REVIEW_STATE} the PR" else - ACTION="${{ github.event.action }}" - TITLE="PR ${ACTION^}: ${{ github.event.pull_request.title }}" - if [[ "$ACTION" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then - TITLE="PR Merged: ${{ github.event.pull_request.title }}" + TITLE="PR ${EVENT_ACTION^}: ${PR_TITLE}" + if [[ "$EVENT_ACTION" == "closed" && "$PR_MERGED" == "true" ]]; then + TITLE="PR Merged: ${PR_TITLE}" COLOR="good" - elif [[ "$ACTION" == "opened" ]]; then + elif [[ "$EVENT_ACTION" == "opened" ]]; then COLOR="#1a73e8" - elif [[ "$ACTION" == "reopened" ]]; then + elif [[ "$EVENT_ACTION" == "reopened" ]]; then COLOR="warning" else COLOR="danger" fi - BODY="${{ github.event.pull_request.user.login }} ${ACTION} the PR" + BODY="${PR_USER} ${EVENT_ACTION} the PR" fi echo "title=${TITLE}" >> $GITHUB_OUTPUT From 47394720e4fa8039c991dce4cbf6c359ec5418c2 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Mon, 18 May 2026 20:23:12 -0400 Subject: [PATCH 5/5] fix(security): remove hardcoded secure_logging override, add no_log to credential tasks --- .gitignore | 4 ++++ CHANGELOG.md | 10 ++++++++++ inventory.yml | 2 +- roles/aap_deploy/tasks/install.yml | 1 + roles/aap_seed/tasks/main.yml | 4 ++-- roles/bootstrap/tasks/aap_subscription.yml | 1 + .../files/crb_migration_factory_aap_cluster_admin.yaml | 1 + roles/mtv_management/tasks/mtv_query_inventory.yml | 1 + roles/vm_hot_plug/tasks/_storage.yml | 1 + roles/vm_lifecycle/tasks/_perform_operation.yml | 1 + 10 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 75ab102..f49ecaf 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,7 @@ cython_debug/ *.sarif **tar.gz + +# Secrets and certificates +*.pem +*.key diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d18df5..9d52b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## v1.22.1 (2026-05-18) + +### Security + +- Remove hardcoded `secure_logging: false` override in aap_seed role; role default of `true` now applies +- Add `no_log: true` to all `ansible.builtin.uri` tasks that pass Authorization headers or credentials +- Document cluster-admin risk in ClusterRoleBinding for migration-factory-aap service account +- Fix inverted secure_logging comment in inventory.yml +- Add `.env`, `*.pem`, `*.key` patterns to .gitignore + ## v1.22.0 (2026-04-02) ### Bug Fixes diff --git a/inventory.yml b/inventory.yml index 677b9bf..9f99a8f 100644 --- a/inventory.yml +++ b/inventory.yml @@ -16,7 +16,7 @@ all: aap_validate_certs: false controller_validate_certs: false - # If secure_logging is set to 'true', Secrets may be displayed in logs. + # If secure_logging is set to 'true', secrets are hidden from logs. # secure_logging: false ## Operators to deploy on the OpenShift Hub Cluster diff --git a/roles/aap_deploy/tasks/install.yml b/roles/aap_deploy/tasks/install.yml index 02b9fed..357286d 100644 --- a/roles/aap_deploy/tasks/install.yml +++ b/roles/aap_deploy/tasks/install.yml @@ -1,5 +1,6 @@ --- - name: install | Validate OpenShift bearer token + no_log: true ansible.builtin.uri: url: "{{ aap_deploy_openshift_host | default(lookup('ansible.builtin.env', 'K8S_AUTH_HOST')) }}" method: GET diff --git a/roles/aap_seed/tasks/main.yml b/roles/aap_seed/tasks/main.yml index a9695bb..4645ed4 100644 --- a/roles/aap_seed/tasks/main.yml +++ b/roles/aap_seed/tasks/main.yml @@ -56,6 +56,7 @@ delay: 5 register: aap_seed_api_status until: aap_seed_api_status.status == 200 + no_log: true ansible.builtin.uri: url: https://{{ aap_seed_controller_hostname }}/api{{ '/controller' if aap_version is not defined or aap_version is defined and aap_version is version('2.5', '>=') }}/v2/config/ # noqa: yaml[line-length] method: GET @@ -72,6 +73,7 @@ delay: 5 register: aap_seed_api_status until: aap_seed_api_status.status == 200 + no_log: true ansible.builtin.uri: url: https://{{ aap_seed_controller_hostname }}/api{{ '/controller' if aap_version is not defined or aap_version is defined and aap_version is version('2.5', '>=') }}/v2/config/ # noqa: yaml[line-length] method: GET @@ -94,9 +96,7 @@ - name: Set variables for {{ aap_seed_cac_collection }} ansible.builtin.set_fact: - controller_configuration_secure_logging: false # noqa: var-naming[no-role-prefix] controller_configuration_async_delay: 5 # noqa: var-naming[no-role-prefix] - aap_configuration_secure_logging: false # noqa: var-naming[no-role-prefix] aap_configuration_async_delay: 5 # noqa: var-naming[no-role-prefix] - name: Call dispatch role diff --git a/roles/bootstrap/tasks/aap_subscription.yml b/roles/bootstrap/tasks/aap_subscription.yml index d2ae563..01c7498 100644 --- a/roles/bootstrap/tasks/aap_subscription.yml +++ b/roles/bootstrap/tasks/aap_subscription.yml @@ -59,6 +59,7 @@ register: __bootstrap_aap_license_manifest_content - name: aap_subscription | Apply license to AAP + no_log: true ansible.builtin.uri: method: POST status_code: 200 diff --git a/roles/create_mf_aap_token/files/crb_migration_factory_aap_cluster_admin.yaml b/roles/create_mf_aap_token/files/crb_migration_factory_aap_cluster_admin.yaml index fd7512d..651936f 100644 --- a/roles/create_mf_aap_token/files/crb_migration_factory_aap_cluster_admin.yaml +++ b/roles/create_mf_aap_token/files/crb_migration_factory_aap_cluster_admin.yaml @@ -1,4 +1,5 @@ --- +# WARNING: cluster-admin grants unrestricted cluster access. Replace with a scoped ClusterRole for production use. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: diff --git a/roles/mtv_management/tasks/mtv_query_inventory.yml b/roles/mtv_management/tasks/mtv_query_inventory.yml index 27a6f15..5a3e31b 100644 --- a/roles/mtv_management/tasks/mtv_query_inventory.yml +++ b/roles/mtv_management/tasks/mtv_query_inventory.yml @@ -81,6 +81,7 @@ headers: Authorization: Bearer {{ openshift_api_key }} register: _mtv_management_mtv_inventory_query_result + no_log: true - name: mtv_query_inventory | Set Result Fact ansible.builtin.set_fact: diff --git a/roles/vm_hot_plug/tasks/_storage.yml b/roles/vm_hot_plug/tasks/_storage.yml index 8060e3d..d9846b5 100644 --- a/roles/vm_hot_plug/tasks/_storage.yml +++ b/roles/vm_hot_plug/tasks/_storage.yml @@ -19,6 +19,7 @@ default([]) | selectattr('name', 'equalto', vm_hot_plug_storage_instance.name) | list | length == 0 ) + no_log: true ansible.builtin.uri: url: "{{ vm_hot_plug_openshift_host }}/apis/subresources.{{ vm_hot_plug_kubevirt_api_version }}\ diff --git a/roles/vm_lifecycle/tasks/_perform_operation.yml b/roles/vm_lifecycle/tasks/_perform_operation.yml index cedb815..2926258 100644 --- a/roles/vm_lifecycle/tasks/_perform_operation.yml +++ b/roles/vm_lifecycle/tasks/_perform_operation.yml @@ -1,6 +1,7 @@ --- - name: _perform_operation | Perform VM Operation + no_log: true ansible.builtin.uri: url: "{{ vm_lifecycle_openshift_host }}/apis/subresources.{{ vm_lifecycle_kubevirt_api_version }}/namespaces/{{ vm_operations_vm.vm.metadata.namespace }}/virtualmachines/{{ vm_operations_vm.vm.metadata.name }}/{{ vm_lifecycle_valid_vm_operations[vm_operations_vm['operation']].endpoint }}" # noqa: yaml[line-length] validate_certs: "{{ vm_lifecycle_openshift_verify_ssl }}"