3 Commits

Author SHA1 Message Date
6fcf6a1ab6 feat(keycloak): add automation service account client support
Introduce a confidential service-account client (Option A) to replace user-based
kcadm sessions. The client is created automatically, granted realm-admin role,
and used for all subsequent Keycloak updates. Includes improved error handling
for HTTP 401 responses.

Discussion: https://chatgpt.com/share/68e01da3-39fc-800f-81be-2d0c8efd81a1
2025-10-03 21:02:16 +02:00
4d9890406e fix(sys-ctl-hlth-csp): ensure '--' separator is added when passing ignore list to checkcsp
Updated README to reflect correct usage with '--', adjusted script.py to always append separator, and simplified task template handling for consistency.

Ref: https://chatgpt.com/share/68dfc69b-7c94-800f-871b-3525deb8e374
2025-10-03 20:50:49 +02:00
59b652958f feat(sys-ctl-hlth-csp): add support for ignoring network block domains
Introduced new variable HEALTH_CSP_IGNORE_NETWORK_BLOCKS_FROM (list, default [])
to suppress network block reports (e.g., ORB) from specific external domains.
Updated script.py to accept and forward the flag, extended systemd exec command
in tasks, added defaults, and documented usage in README.

Ref: https://chatgpt.com/share/68dfc69b-7c94-800f-871b-3525deb8e374
2025-10-03 15:23:57 +02:00
9 changed files with 168 additions and 11 deletions

View File

@@ -14,6 +14,32 @@ Designed for Archlinux systems, this role periodically checks whether web resour
- **Domain Extraction:** Parses all `.conf` files in the NGINX config folder to determine the list of domains to check.
- **Automated Execution:** Registers a systemd service and timer for recurring health checks.
- **Error Notification:** Integrates with `sys-ctl-alm-compose` for alerting on failure.
- **Ignore List Support:** Optional variable to suppress network block reports from specific external domains.
## Configuration
### Variables
- **`HEALTH_CSP_IGNORE_NETWORK_BLOCKS_FROM`** (list, default: `[]`)
Optional list of domains whose network block failures (e.g., ORB) should be ignored during CSP checks.
Example:
```yaml
HEALTH_CSP_IGNORE_NETWORK_BLOCKS_FROM:
- pxscdn.com
- cdn.example.org
```
This will run the CSP checker with:
```bash
checkcsp start --short --ignore-network-blocks-from pxscdn.com -- cdn.example.org <domains...>
```
### Systemd Integration
The role configures a systemd service and timer which executes the CSP crawler periodically against all NGINX domains.
## License
@@ -24,4 +50,4 @@ Infinito.Nexus NonCommercial License
Kevin Veen-Birkenbach
Consulting & Coaching Solutions
[https://www.veen.world](https://www.veen.world)
[https://www.veen.world](https://www.veen.world)

View File

@@ -0,0 +1,5 @@
# List of domains whose network block failures (e.g., ORB) should be ignored
# during CSP checks. This is useful for suppressing known external resources
# (e.g., third-party CDNs) that cannot be influenced but otherwise cause
# unnecessary alerts in the crawler reports.
HEALTH_CSP_IGNORE_NETWORK_BLOCKS_FROM: []

View File

@@ -21,11 +21,20 @@ def extract_domains(config_path):
print(f"Directory {config_path} not found.", file=sys.stderr)
return None
def run_checkcsp(domains):
def run_checkcsp(domains, ignore_network_blocks_from):
"""
Executes the 'checkcsp' command with the given domains.
Executes the 'checkcsp' command with the given domains and optional ignores.
"""
cmd = ["checkcsp", "start", "--short"] + domains
cmd = ["checkcsp", "start", "--short"]
# pass through ignore list only if not empty
if ignore_network_blocks_from:
cmd.append("--ignore-network-blocks-from")
cmd.extend(ignore_network_blocks_from)
cmd.append("--")
cmd += domains
try:
result = subprocess.run(cmd, check=True)
return result.returncode
@@ -45,6 +54,12 @@ def main():
required=True,
help="Directory containing NGINX .conf files"
)
parser.add_argument(
"--ignore-network-blocks-from",
nargs="*",
default=[],
help="Optional: one or more domains whose network block failures should be ignored"
)
args = parser.parse_args()
domains = extract_domains(args.nginx_config_dir)
@@ -55,7 +70,7 @@ def main():
print("No domains found to check.")
sys.exit(0)
rc = run_checkcsp(domains)
rc = run_checkcsp(domains, args.ignore_network_blocks_from)
sys.exit(rc)
if __name__ == "__main__":

View File

@@ -18,6 +18,9 @@
system_service_timer_enabled: true
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
system_service_tpl_timeout_start_sec: "{{ CURRENT_PLAY_DOMAINS_ALL | timeout_start_sec_for_domains }}"
system_service_tpl_exec_start: "{{ system_service_script_exec }} --nginx-config-dir={{ NGINX.DIRECTORIES.HTTP.SERVERS }}"
system_service_tpl_exec_start: >-
{{ system_service_script_exec }}
--nginx-config-dir={{ NGINX.DIRECTORIES.HTTP.SERVERS }}
--ignore-network-blocks-from {{ HEALTH_CSP_IGNORE_NETWORK_BLOCKS_FROM | join(' ') }}
- include_tasks: utils/run_once.yml

View File

@@ -1,6 +1,7 @@
load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing
load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing
actions:
import_realm: True # Import REALM
import_realm: True # Import REALM
create_automation_client: True
features:
matomo: true
css: true

View File

@@ -21,6 +21,7 @@
shell: "{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json"
register: all_scopes
changed_when: false
failed_when: "'HTTP 401' in (all_scopes.stderr | default(''))"
- name: Extract RBAC scope id
set_fact:

View File

@@ -0,0 +1,63 @@
# Creates a confidential client with service account, fetches the secret,
# and grants realm-management/realm-admin to its service-account user.
- name: "Ensure automation client exists (confidential + service accounts)"
shell: |
{{ KEYCLOAK_EXEC_KCADM }} create clients -r {{ KEYCLOAK_REALM }} \
-s clientId={{ KEYCLOAK_AUTOMATION_CLIENT_ID }} \
-s protocol=openid-connect \
-s publicClient=false \
-s serviceAccountsEnabled=true \
-s directAccessGrantsEnabled=false
register: create_client
changed_when: create_client.rc == 0
failed_when: create_client.rc != 0 and ('already exists' not in (create_client.stderr | lower))
- name: "Resolve automation client id"
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get clients -r {{ KEYCLOAK_REALM }}
--query 'clientId={{ KEYCLOAK_AUTOMATION_CLIENT_ID }}' --fields id --format json | jq -r '.[0].id'
register: auto_client_id_cmd
changed_when: false
- name: "Fail if client id could not be resolved"
assert:
that:
- "(auto_client_id_cmd.stdout | trim) is match('^[0-9a-f-]+$')"
fail_msg: "Automation client id could not be resolved."
- name: "Read client secret"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get clients/{{ auto_client_id_cmd.stdout | trim }}/client-secret
-r {{ KEYCLOAK_REALM }} --format json | jq -r .value
register: auto_client_secret_cmd
changed_when: false
- name: "Expose client secret as a fact"
set_fact:
KEYCLOAK_AUTOMATION_CLIENT_SECRET: "{{ auto_client_secret_cmd.stdout | trim }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Grant {{ KEYCLOAK_AUTOMATION_GRANT_ROLE }} to service account"
shell: >
{{ KEYCLOAK_EXEC_KCADM }} add-roles -r {{ KEYCLOAK_REALM }}
--uusername service-account-{{ KEYCLOAK_AUTOMATION_CLIENT_ID }}
--cclientid realm-management
--rolename {{ KEYCLOAK_AUTOMATION_GRANT_ROLE }}
register: grant_role
changed_when: grant_role.rc == 0
failed_when: grant_role.rc != 0 and ('already exists' not in (grant_role.stderr | lower))
- name: "Verify client-credentials login works"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: >
{{ KEYCLOAK_EXEC_KCADM }} config credentials
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
--realm {{ KEYCLOAK_REALM }}
--client {{ KEYCLOAK_AUTOMATION_CLIENT_ID }}
--client-secret {{ KEYCLOAK_AUTOMATION_CLIENT_SECRET }} &&
{{ KEYCLOAK_EXEC_KCADM }} get realms/{{ KEYCLOAK_REALM }} --format json | jq -r '.realm'
register: verify_cc
changed_when: false
failed_when: (verify_cc.rc != 0) or ((verify_cc.stdout | trim) != (KEYCLOAK_REALM | trim))

View File

@@ -36,6 +36,42 @@
--password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
changed_when: false
- name: Verify kcadm session works (quick read)
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get realms --format json | jq -r '.[0].realm' | head -n1
register: kcadm_verify
changed_when: false
failed_when: >
(kcadm_verify.rc != 0)
or ('HTTP 401' in (kcadm_verify.stderr | default('')))
or ((kcadm_verify.stdout | trim) == '')
# --- Create & grant automation service account (Option A) ---
- name: "Ensure automation service account client (Option A)"
include_tasks: 05a_service_account.yml
when: applications | get_app_conf(application_id, 'actions.create_automation_client', True)
# --- Switch session to the service account for all subsequent API work ---
- name: kcadm login (realm) using service account
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: >
{{ KEYCLOAK_EXEC_KCADM }} config credentials
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
--realm {{ KEYCLOAK_REALM }}
--client {{ KEYCLOAK_AUTOMATION_CLIENT_ID }}
--client-secret {{ KEYCLOAK_AUTOMATION_CLIENT_SECRET }}
changed_when: false
- name: Verify kcadm session works (exact realm via service account)
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get realms/{{ KEYCLOAK_REALM }} --format json | jq -r '.realm'
register: kcadm_verify_sa
changed_when: false
failed_when: >
(kcadm_verify_sa.rc != 0)
or ('HTTP 401' in (kcadm_verify_sa.stderr | default('')))
or ((kcadm_verify_sa.stdout | trim) != (KEYCLOAK_REALM | trim))
- name: "Update Client settings"
vars:
kc_object_kind: "client"

View File

@@ -1,6 +1,6 @@
# General
application_id: "web-app-keycloak" # Internal Infinito.Nexus application id
database_type: "postgres" # Database which will be used
application_id: "web-app-keycloak" # Internal Infinito.Nexus application id
database_type: "postgres" # Database which will be used
# Keycloak
@@ -34,9 +34,16 @@ KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_
## Docker
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
KEYCLOAK_KCADM_CONFIG: "/opt/keycloak/data/kcadm.config"
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh --config {{ KEYCLOAK_KCADM_CONFIG }}"
## Automation Service Account (Option A)
KEYCLOAK_AUTOMATION_CLIENT_ID: "infinito-automation"
KEYCLOAK_AUTOMATION_GRANT_ROLE: "realm-admin" # or granular roles if you prefer
# Will be discovered dynamically and set as a fact during the run:
# KEYCLOAK_AUTOMATION_CLIENT_SECRET
## Server
KEYCLOAK_SERVER_HOST: "127.0.0.1:{{ ports.localhost.http[application_id] }}"