diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
new file mode 100644
index 0000000..2975c5a
--- /dev/null
+++ b/.github/workflows/gitleaks.yml
@@ -0,0 +1,29 @@
+---
+name: Gitleaks
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ 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
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ 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/.github/workflows/slack-pr-notifications.yml b/.github/workflows/slack-pr-notifications.yml
new file mode 100644
index 0000000..d3498d3
--- /dev/null
+++ b/.github/workflows/slack-pr-notifications.yml
@@ -0,0 +1,69 @@
+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
+ 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 [[ "$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
+ TITLE="PR ${EVENT_ACTION^}: ${PR_TITLE}"
+ if [[ "$EVENT_ACTION" == "closed" && "$PR_MERGED" == "true" ]]; then
+ TITLE="PR Merged: ${PR_TITLE}"
+ COLOR="good"
+ elif [[ "$EVENT_ACTION" == "opened" ]]; then
+ COLOR="#1a73e8"
+ elif [[ "$EVENT_ACTION" == "reopened" ]]; then
+ COLOR="warning"
+ else
+ COLOR="danger"
+ fi
+ BODY="${PR_USER} ${EVENT_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>"
+ }
+ }
+ ]
+ }
+ ]
+ }
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/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..972b0b5
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,70 @@
+# 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$''',
+ '''README\.md$''',
+]
+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*#.*''',
+ # 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
+[[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''', '''.*''', '''[>|][+-]?\s*$''']
+paths = ['''defaults/main\.yml$''', '''inventory\.yml$''', '''README\.md$''', '''tasks/main\.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$''', '''README\.md$''']
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 }}"