From 93be9bb00d44ac10794484e7f2a51fe31c6c9119 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Thu, 28 May 2026 08:35:13 +0200 Subject: [PATCH 1/9] add a var-defaults[no-empty] check that ensures no defaults are empty --- .ansible-lint | 3 + .ansible-lint-rules/__init__.py | 0 .ansible-lint-rules/conftest.py | 3 + .ansible-lint-rules/empty_defaults.py | 92 +++++++++++++++++++ .../test_empty_defaults/defaults/main.yml | 12 +++ .../roles/test_empty_defaults/tasks/main.yml | 4 + .../roles/test_empty_defaults/vars/main.yml | 3 + 7 files changed, 117 insertions(+) create mode 100644 .ansible-lint-rules/__init__.py create mode 100644 .ansible-lint-rules/conftest.py create mode 100644 .ansible-lint-rules/empty_defaults.py create mode 100644 tests/fixtures/ansible-lint/roles/test_empty_defaults/defaults/main.yml create mode 100644 tests/fixtures/ansible-lint/roles/test_empty_defaults/tasks/main.yml create mode 100644 tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml diff --git a/.ansible-lint b/.ansible-lint index 5e30b9bf6..f7c8dbe54 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -1,4 +1,7 @@ --- +use_default_rules: true +rulesdir: + - .ansible-lint-rules/ enable_list: - var-naming[no-role-prefix] exclude_paths: diff --git a/.ansible-lint-rules/__init__.py b/.ansible-lint-rules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/.ansible-lint-rules/conftest.py b/.ansible-lint-rules/conftest.py new file mode 100644 index 000000000..c186565df --- /dev/null +++ b/.ansible-lint-rules/conftest.py @@ -0,0 +1,3 @@ +"""Makes ansible-lint pytest fixtures available for inline rule tests.""" + +from ansiblelint.testing.fixtures import * # noqa: F403 diff --git a/.ansible-lint-rules/empty_defaults.py b/.ansible-lint-rules/empty_defaults.py new file mode 100644 index 000000000..abf30b047 --- /dev/null +++ b/.ansible-lint-rules/empty_defaults.py @@ -0,0 +1,92 @@ +"""Implementation of var-defaults rule.""" + +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING + +from ansiblelint.rules import AnsibleLintRule +from ansiblelint.utils import parse_yaml_from_file + +if TYPE_CHECKING: + from ansiblelint.errors import MatchError + from ansiblelint.file_utils import Lintable + + +class EmptyDefaultsRule(AnsibleLintRule): + """Role default variables should not have empty values.""" + + id = "var-defaults" + severity = "HIGH" + tags = ["idiom"] + version_added = "custom" + + _ids = { + "var-defaults[no-empty]": "Role default variables must not be null or empty strings.", + } + + def matchyaml(self, file: Lintable) -> list[MatchError]: + """Return matches for empty defaults in role defaults files.""" + results: list[MatchError] = [] + + if str(file.kind) != "vars" or not file.data: + return results + + if not file.role or "defaults" not in file.path.parts: + return results + + meta_data = parse_yaml_from_file(str(file.path)) + if not isinstance(meta_data, dict): + return results + + for key, value in meta_data.items(): + if value is None or value == "": + results.append( + self.create_matcherror( + message=f"Role default variable '{key}' has an empty value. Use `undef(hint='…')` to indicate defaults that need to be overriden.", + filename=file, + tag="var-defaults[no-empty]", + data=key, + ), + ) + + return results + + +if "pytest" in sys.modules: + from ansiblelint.config import Options + from ansiblelint.file_utils import Lintable + from ansiblelint.rules import RulesCollection + from ansiblelint.runner import Runner + + def test_empty_defaults_are_flagged( + config_options: Options, + app: object, + ) -> None: + """Null and empty string defaults produce match errors.""" + rules = RulesCollection(app=app, options=config_options) + rules.register(EmptyDefaultsRule()) + results = Runner( + Lintable("tests/fixtures/ansible-lint/roles/test_empty_defaults"), + rules=rules, + ).run() + empty_results = [r for r in results if r.rule.id == EmptyDefaultsRule.id] + assert len(empty_results) == 4 + for result in empty_results: + assert result.tag == "var-defaults[no-empty]" + + def test_vars_file_not_checked( + config_options: Options, + app: object, + ) -> None: + """Vars files are not checked, only defaults.""" + rules = RulesCollection(app=app, options=config_options) + rules.register(EmptyDefaultsRule()) + results = Runner( + Lintable( + "tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml" + ), + rules=rules, + ).run() + empty_results = [r for r in results if r.rule.id == EmptyDefaultsRule.id] + assert len(empty_results) == 0 diff --git a/tests/fixtures/ansible-lint/roles/test_empty_defaults/defaults/main.yml b/tests/fixtures/ansible-lint/roles/test_empty_defaults/defaults/main.yml new file mode 100644 index 000000000..8dd6aaf82 --- /dev/null +++ b/tests/fixtures/ansible-lint/roles/test_empty_defaults/defaults/main.yml @@ -0,0 +1,12 @@ +--- +test_empty_defaults_null_var: +test_empty_defaults_another_null: +test_empty_defaults_empty_string: "" +test_empty_defaults_another_empty_string: '' +test_empty_defaults_name: "some_value" +test_empty_defaults_port: 8080 +test_empty_defaults_enabled: false +test_empty_defaults_items: + - one + - two +test_empty_defaults_plugins: [] diff --git a/tests/fixtures/ansible-lint/roles/test_empty_defaults/tasks/main.yml b/tests/fixtures/ansible-lint/roles/test_empty_defaults/tasks/main.yml new file mode 100644 index 000000000..9a8e9facf --- /dev/null +++ b/tests/fixtures/ansible-lint/roles/test_empty_defaults/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- name: Placeholder task + ansible.builtin.debug: + msg: "test" diff --git a/tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml b/tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml new file mode 100644 index 000000000..75ac4445f --- /dev/null +++ b/tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml @@ -0,0 +1,3 @@ +--- +test_empty_defaults_internal: +test_empty_defaults_internal_empty: "" From 64545cf3943423ac9287019deba21e53216c486e Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Thu, 28 May 2026 12:40:21 +0200 Subject: [PATCH 2/9] add no-static-secrets ansible-lint rule --- .ansible-lint-rules/no_static_secrets.py | 115 ++++++++++++++++++ .../test_static_secrets/defaults/main.yml | 10 ++ .../roles/test_static_secrets/tasks/main.yml | 4 + .../roles/test_static_secrets/vars/main.yml | 4 + tests/fixtures/ansible-lint/vars/secrets.yml | 7 ++ 5 files changed, 140 insertions(+) create mode 100644 .ansible-lint-rules/no_static_secrets.py create mode 100644 tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml create mode 100644 tests/fixtures/ansible-lint/roles/test_static_secrets/tasks/main.yml create mode 100644 tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml create mode 100644 tests/fixtures/ansible-lint/vars/secrets.yml diff --git a/.ansible-lint-rules/no_static_secrets.py b/.ansible-lint-rules/no_static_secrets.py new file mode 100644 index 000000000..b51bb30fc --- /dev/null +++ b/.ansible-lint-rules/no_static_secrets.py @@ -0,0 +1,115 @@ +"""Implementation of var-secrets rule.""" + +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING + +from ansiblelint.rules import AnsibleLintRule +from ansiblelint.text import has_jinja +from ansiblelint.utils import parse_yaml_from_file + +if TYPE_CHECKING: + from ansiblelint.errors import MatchError + from ansiblelint.file_utils import Lintable + +SECRET_SUFFIXES = ( + "_password", + "_passwd", + "_secret", + "_token", +) + + +class NoStaticSecretsRule(AnsibleLintRule): + """Variables that look like secrets must not have static default values.""" + + id = "var-secrets" + severity = "HIGH" + tags = ["security"] + version_added = "custom" + + _ids = { + "var-secrets[no-static]": "Secret variables must use Jinja expressions, not static strings.", + } + + @staticmethod + def _looks_like_secret(name: str) -> bool: + return any(name.endswith(suffix) for suffix in SECRET_SUFFIXES) + + def matchyaml(self, file: Lintable) -> list[MatchError]: + """Flag secret-looking variables with static string values.""" + results: list[MatchError] = [] + + if str(file.kind) != "vars" or not file.data: + return results + + meta_data = parse_yaml_from_file(str(file.path)) + if not isinstance(meta_data, dict): + return results + + for key, value in meta_data.items(): + if not self._looks_like_secret(str(key)): + continue + if isinstance(value, str) and not has_jinja(value): + results.append( + self.create_matcherror( + message=f"Secret variable '{key}' has a static value. Use a Jinja expression instead.", + filename=file, + tag="var-secrets[no-static]", + data=key, + ), + ) + + return results + + +if "pytest" in sys.modules: + from ansiblelint.config import Options + from ansiblelint.file_utils import Lintable + from ansiblelint.rules import RulesCollection + from ansiblelint.runner import Runner + + def _run_rule(path: str, config_options: Options, app: object) -> list: + rules = RulesCollection(app=app, options=config_options) + rules.register(NoStaticSecretsRule()) + results = Runner(Lintable(path), rules=rules).run() + return [r for r in results if r.rule.id == NoStaticSecretsRule.id] + + def test_static_secrets_flagged( + config_options: Options, + app: object, + ) -> None: + """Static secret values produce match errors.""" + results = _run_rule( + "tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml", + config_options, + app, + ) + assert len(results) == 2 + for result in results: + assert result.tag == "var-secrets[no-static]" + + def test_jinja_secrets_pass( + config_options: Options, + app: object, + ) -> None: + """Jinja expression secrets are not flagged.""" + results = _run_rule( + "tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml", + config_options, + app, + ) + assert len(results) == 0 + + def test_non_role_vars_checked( + config_options: Options, + app: object, + ) -> None: + """Non-role vars files are also checked.""" + results = _run_rule( + "tests/fixtures/ansible-lint/vars/secrets.yml", + config_options, + app, + ) + assert len(results) == 1 diff --git a/tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml b/tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml new file mode 100644 index 000000000..70c034b22 --- /dev/null +++ b/tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml @@ -0,0 +1,10 @@ +--- +# Should be flagged (static secrets) +test_static_secrets_database_password: CHANGEME +test_static_secrets_oauth_secret: "my-secret" +# Should NOT be flagged (Jinja expression) +test_static_secrets_admin_password: "{{ lookup('ansible.builtin.password', '/tmp/passwd') }}" +test_static_secrets_api_token: "{{ some_other_token }}" +# Should NOT be flagged (not a secret name) +test_static_secrets_hostname: "example.com" +test_static_secrets_port: 8080 diff --git a/tests/fixtures/ansible-lint/roles/test_static_secrets/tasks/main.yml b/tests/fixtures/ansible-lint/roles/test_static_secrets/tasks/main.yml new file mode 100644 index 000000000..9a8e9facf --- /dev/null +++ b/tests/fixtures/ansible-lint/roles/test_static_secrets/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- name: Placeholder task + ansible.builtin.debug: + msg: "test" diff --git a/tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml b/tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml new file mode 100644 index 000000000..4d0ad8f40 --- /dev/null +++ b/tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml @@ -0,0 +1,4 @@ +--- +# All Jinja - should NOT be flagged +test_static_secrets_ca_password: "{{ ca_key_password }}" +test_static_secrets_consumer_secret: "{{ lookup('ansible.builtin.password', '/tmp/secret') }}" diff --git a/tests/fixtures/ansible-lint/vars/secrets.yml b/tests/fixtures/ansible-lint/vars/secrets.yml new file mode 100644 index 000000000..3d43b3374 --- /dev/null +++ b/tests/fixtures/ansible-lint/vars/secrets.yml @@ -0,0 +1,7 @@ +--- +# Should be flagged (static secret in non-role vars file) +some_database_password: CHANGEME +# Should NOT be flagged (Jinja expression) +some_other_password: "{{ generated_password }}" +# Should NOT be flagged (not a secret name) +some_database_name: mydb From 719feb88a0acce41f89196cf7ae5f1b70b96c7b9 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Thu, 28 May 2026 14:10:04 +0200 Subject: [PATCH 3/9] fix static default passwords in iop roles --- src/roles/iop_advisor/defaults/main.yaml | 2 +- src/roles/iop_inventory/defaults/main.yaml | 2 +- src/roles/iop_remediation/defaults/main.yaml | 2 +- src/roles/iop_vmaas/defaults/main.yaml | 2 +- src/roles/iop_vulnerability/defaults/main.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/roles/iop_advisor/defaults/main.yaml b/src/roles/iop_advisor/defaults/main.yaml index 52645e1d1..a5722ba2c 100644 --- a/src/roles/iop_advisor/defaults/main.yaml +++ b/src/roles/iop_advisor/defaults/main.yaml @@ -5,6 +5,6 @@ iop_advisor_registry_auth_file: /etc/foreman/registry-auth.json iop_advisor_database_name: advisor_db iop_advisor_database_user: advisor_user -iop_advisor_database_password: CHANGEME +iop_advisor_database_password: "{{ undef(hint='Set a secure database password') }}" iop_advisor_database_host: host.containers.internal iop_advisor_database_port: 5432 diff --git a/src/roles/iop_inventory/defaults/main.yaml b/src/roles/iop_inventory/defaults/main.yaml index b287bbf78..66ce97c79 100644 --- a/src/roles/iop_inventory/defaults/main.yaml +++ b/src/roles/iop_inventory/defaults/main.yaml @@ -5,6 +5,6 @@ iop_inventory_registry_auth_file: /etc/foreman/registry-auth.json iop_inventory_database_name: inventory_db iop_inventory_database_user: inventory_admin -iop_inventory_database_password: CHANGEME +iop_inventory_database_password: "{{ undef(hint='Set a secure database password') }}" iop_inventory_database_host: host.containers.internal iop_inventory_database_port: 5432 diff --git a/src/roles/iop_remediation/defaults/main.yaml b/src/roles/iop_remediation/defaults/main.yaml index 99bceb8e9..987b83430 100644 --- a/src/roles/iop_remediation/defaults/main.yaml +++ b/src/roles/iop_remediation/defaults/main.yaml @@ -5,6 +5,6 @@ iop_remediation_registry_auth_file: /etc/foreman/registry-auth.json iop_remediation_database_name: remediations_db iop_remediation_database_user: remediations_user -iop_remediation_database_password: CHANGEME +iop_remediation_database_password: "{{ undef(hint='Set a secure database password') }}" iop_remediation_database_host: "host.containers.internal" iop_remediation_database_port: "5432" diff --git a/src/roles/iop_vmaas/defaults/main.yaml b/src/roles/iop_vmaas/defaults/main.yaml index 2d5f0511f..9e681d43a 100644 --- a/src/roles/iop_vmaas/defaults/main.yaml +++ b/src/roles/iop_vmaas/defaults/main.yaml @@ -5,7 +5,7 @@ iop_vmaas_registry_auth_file: /etc/foreman/registry-auth.json iop_vmaas_database_name: vmaas_db iop_vmaas_database_user: vmaas_admin -iop_vmaas_database_password: CHANGEME +iop_vmaas_database_password: "{{ undef(hint='Set a secure database password') }}" iop_vmaas_database_host: "host.containers.internal" iop_vmaas_database_port: "5432" diff --git a/src/roles/iop_vulnerability/defaults/main.yaml b/src/roles/iop_vulnerability/defaults/main.yaml index 4811acf93..42c1b12f4 100644 --- a/src/roles/iop_vulnerability/defaults/main.yaml +++ b/src/roles/iop_vulnerability/defaults/main.yaml @@ -5,7 +5,7 @@ iop_vulnerability_registry_auth_file: /etc/foreman/registry-auth.json iop_vulnerability_database_name: vulnerability_db iop_vulnerability_database_user: vulnerability_admin -iop_vulnerability_database_password: CHANGEME +iop_vulnerability_database_password: "{{ undef(hint='Set a secure database password') }}" iop_vulnerability_database_host: "host.containers.internal" iop_vulnerability_database_port: "5432" From 78f0805dffffbafbd318d242388ed8b24a61fe95 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Thu, 28 May 2026 14:57:58 +0200 Subject: [PATCH 4/9] fix static default password in postgresql role --- src/roles/postgresql/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roles/postgresql/defaults/main.yml b/src/roles/postgresql/defaults/main.yml index 7c80c3a68..82d1eb667 100644 --- a/src/roles/postgresql/defaults/main.yml +++ b/src/roles/postgresql/defaults/main.yml @@ -8,7 +8,7 @@ postgresql_restart_policy: always postgresql_data_dir: /var/lib/pgsql/data -postgresql_admin_password: "CHANGEME" +postgresql_admin_password: "{{ undef(hint='Set a secure database password') }}" postgresql_max_connections: 500 postgresql_shared_buffers: 512MB From 1b054ec4577bdf0429f2bcd51aa160c23f9f8069 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Thu, 28 May 2026 14:58:18 +0200 Subject: [PATCH 5/9] allow empty ca paths --- src/roles/candlepin/defaults/main.yml | 4 +--- src/roles/foreman/defaults/main.yaml | 2 +- src/roles/hammer/defaults/main.yml | 2 +- src/roles/pulp/defaults/main.yaml | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/roles/candlepin/defaults/main.yml b/src/roles/candlepin/defaults/main.yml index a0a8b15b4..acac7570f 100644 --- a/src/roles/candlepin/defaults/main.yml +++ b/src/roles/candlepin/defaults/main.yml @@ -20,7 +20,5 @@ candlepin_database_host: localhost candlepin_database_port: 5432 candlepin_database_ssl: false candlepin_database_ssl_mode: disable -candlepin_database_ssl_ca: +candlepin_database_ssl_ca: # noqa: var-defaults[no-empty] candlepin_database_ssl_ca_path: /etc/candlepin/certs/db-ca.crt -candlepin_database_ssl_cert: -candlepin_database_ssl_key: diff --git a/src/roles/foreman/defaults/main.yaml b/src/roles/foreman/defaults/main.yaml index fad6b1161..fc39c789d 100644 --- a/src/roles/foreman/defaults/main.yaml +++ b/src/roles/foreman/defaults/main.yaml @@ -9,7 +9,7 @@ foreman_database_host: localhost foreman_database_port: 5432 foreman_database_pool: 9 foreman_database_ssl_mode: disable -foreman_database_ssl_ca: +foreman_database_ssl_ca: # noqa: var-defaults[no-empty] foreman_database_ssl_ca_path: /etc/foreman/db-ca.crt foreman_name: "{{ ansible_facts['fqdn'] }}" diff --git a/src/roles/hammer/defaults/main.yml b/src/roles/hammer/defaults/main.yml index 3a089cf91..0949b20fd 100644 --- a/src/roles/hammer/defaults/main.yml +++ b/src/roles/hammer/defaults/main.yml @@ -1,6 +1,6 @@ --- hammer_foreman_server_url: "https://{{ ansible_facts['fqdn'] }}" -hammer_ca_certificate: "" +hammer_ca_certificate: "" # noqa: var-defaults[no-empty] hammer_default_plugins: - foreman hammer_plugins: [] diff --git a/src/roles/pulp/defaults/main.yaml b/src/roles/pulp/defaults/main.yaml index 2d33986b7..fa6a81a11 100644 --- a/src/roles/pulp/defaults/main.yaml +++ b/src/roles/pulp/defaults/main.yaml @@ -45,7 +45,7 @@ pulp_database_user: pulp pulp_database_host: localhost pulp_database_port: 5432 pulp_database_ssl_mode: disabled -pulp_database_ssl_ca: +pulp_database_ssl_ca: # noqa: var-defaults[no-empty] pulp_database_ssl_ca_path: /etc/pulp/certs/db-ca.crt pulp_settings_database_env: From a9de1aa7f12c6d9ec5417fc93f27517a0472ad8f Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Wed, 27 May 2026 13:04:30 +0200 Subject: [PATCH 6/9] Generate passwords instead of using hard-coded ones --- docs/developer/playbooks-and-roles.md | 39 +++++++++++++++++++++++++++ src/vars/base.yaml | 9 ++++--- src/vars/database.yml | 12 ++++++--- src/vars/database_iop.yml | 15 +++++++---- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/developer/playbooks-and-roles.md b/docs/developer/playbooks-and-roles.md index aa2a1a932..082a434e8 100644 --- a/docs/developer/playbooks-and-roles.md +++ b/docs/developer/playbooks-and-roles.md @@ -172,6 +172,45 @@ include: - _flavor_features ``` +## Secret Management + +Secrets (passwords, tokens, OAuth secrets) must never be hardcoded. Use the `ansible.builtin.password` lookup to auto-generate secrets and persist them to files under `obsah_state_path`. + +### Pattern + +Every secret needs two variables: a `_file` variable pointing to the state file, and the secret variable itself using a `lookup` against that file. + +```yaml +example_database_password_file: "{{ obsah_state_path }}/example-db-password" +example_database_password: "{{ lookup('ansible.builtin.password', example_database_password_file, chars=['ascii_letters', 'digits']) }}" +``` + +The `lookup` generates a random password on first run and writes it to the file. On subsequent runs, it reads the existing value, ensuring the secret is stable across deploys. + +### Parameters + +| Parameter | Usage | +|-----------|-------| +| `chars` | Character set for generation. Use `['ascii_letters', 'digits']` for passwords safe in URLs and connection strings. | +| `length` | Password length. Defaults to 20 if omitted. Use `length=32` for high-entropy secrets like OAuth tokens. | + +### Where to define secrets + +Define secrets in `src/vars/` files, not in role `defaults/`. Vars files have higher Ansible precedence and are the effective source of truth at deploy time. + +| Secret type | Define in | +|-------------|-----------| +| Database passwords | `src/vars/database.yml` | +| IOP database passwords | `src/vars/database_iop.yml` | +| general passwords/secrets | `src/vars/base.yaml` | +| Foreman-specific passwords/secrets | `src/vars/foreman.yml` | + +### Naming conventions + +- State file variable: `__password_file` (or `_secret_file` for non-password secrets) +- State file path: `{{ obsah_state_path }}/--password` (use hyphens, no underscores) +- Secret variable: `__password` + ## How to Add a New Command 1. Create a directory under `src/playbooks//` (or `development/playbooks/` for dev tools). diff --git a/src/vars/base.yaml b/src/vars/base.yaml index 1926ebc6f..b85b9d02f 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -3,9 +3,12 @@ certificates_hostnames: - "{{ ansible_facts['fqdn'] }}" - localhost -certificates_ca_password: "CHANGEME" -candlepin_keystore_password: "CHANGEME" -candlepin_oauth_secret: "CHANGEME" +certificates_ca_password_file: "{{ obsah_state_path }}/certificates-ca-password" +certificates_ca_password: "{{ lookup('ansible.builtin.password', certificates_ca_password_file, chars=['ascii_letters', 'digits']) }}" +candlepin_keystore_password_file: "{{ obsah_state_path }}/candlepin-keystore-password" +candlepin_keystore_password: "{{ lookup('ansible.builtin.password', candlepin_keystore_password_file, chars=['ascii_letters', 'digits']) }}" +candlepin_oauth_secret_file: "{{ obsah_state_path }}/candlepin-oauth-secret" +candlepin_oauth_secret: "{{ lookup('ansible.builtin.password', candlepin_oauth_secret_file, chars=['ascii_letters', 'digits'], length=32) }}" candlepin_ca_key_password: "{{ ca_key_password }}" candlepin_ca_key: "{{ ca_key }}" diff --git a/src/vars/database.yml b/src/vars/database.yml index 2a89bed01..236e62421 100644 --- a/src/vars/database.yml +++ b/src/vars/database.yml @@ -6,13 +6,19 @@ database_ssl_ca: foreman_database_name: foreman foreman_database_user: foreman -foreman_database_password: CHANGEME +foreman_database_password_file: "{{ obsah_state_path }}/foreman-db-password" +foreman_database_password: "{{ lookup('ansible.builtin.password', foreman_database_password_file, chars=['ascii_letters', 'digits']) }}" candlepin_database_name: candlepin candlepin_database_user: candlepin -candlepin_database_password: CHANGEME +candlepin_database_password_file: "{{ obsah_state_path }}/candlepin-db-password" +candlepin_database_password: "{{ lookup('ansible.builtin.password', candlepin_database_password_file, chars=['ascii_letters', 'digits']) }}" pulp_database_name: pulp pulp_database_user: pulp -pulp_database_password: CHANGEME +pulp_database_password_file: "{{ obsah_state_path }}/pulp-db-password" +pulp_database_password: "{{ lookup('ansible.builtin.password', pulp_database_password_file, chars=['ascii_letters', 'digits']) }}" + +postgresql_admin_password_file: "{{ obsah_state_path }}/postgresql-admin-password" +postgresql_admin_password: "{{ lookup('ansible.builtin.password', postgresql_admin_password_file, chars=['ascii_letters', 'digits']) }}" candlepin_database_host: "{{ database_host }}" candlepin_database_port: "{{ database_port }}" diff --git a/src/vars/database_iop.yml b/src/vars/database_iop.yml index 792333b20..9166b923a 100644 --- a/src/vars/database_iop.yml +++ b/src/vars/database_iop.yml @@ -6,31 +6,36 @@ iop_inventory_database_host: "{{ iop_database_host }}" iop_inventory_database_port: "{{ iop_database_port }}" iop_inventory_database_name: inventory_db iop_inventory_database_user: inventory_admin -iop_inventory_database_password: CHANGEME +iop_inventory_database_password_file: "{{ obsah_state_path }}/iop-inventory-db-password" +iop_inventory_database_password: "{{ lookup('ansible.builtin.password', iop_inventory_database_password_file, chars=['ascii_letters', 'digits']) }}" iop_advisor_database_host: "{{ iop_database_host }}" iop_advisor_database_port: "{{ iop_database_port }}" iop_advisor_database_name: advisor_db iop_advisor_database_user: advisor_user -iop_advisor_database_password: CHANGEME +iop_advisor_database_password_file: "{{ obsah_state_path }}/iop-advisor-db-password" +iop_advisor_database_password: "{{ lookup('ansible.builtin.password', iop_advisor_database_password_file, chars=['ascii_letters', 'digits']) }}" iop_remediation_database_host: "{{ iop_database_host }}" iop_remediation_database_port: "{{ iop_database_port }}" iop_remediation_database_name: remediations_db iop_remediation_database_user: remediations_user -iop_remediation_database_password: CHANGEME +iop_remediation_database_password_file: "{{ obsah_state_path }}/iop-remediation-db-password" +iop_remediation_database_password: "{{ lookup('ansible.builtin.password', iop_remediation_database_password_file, chars=['ascii_letters', 'digits']) }}" iop_vmaas_database_host: "{{ iop_database_host }}" iop_vmaas_database_port: "{{ iop_database_port }}" iop_vmaas_database_name: vmaas_db iop_vmaas_database_user: vmaas_admin -iop_vmaas_database_password: CHANGEME +iop_vmaas_database_password_file: "{{ obsah_state_path }}/iop-vmaas-db-password" +iop_vmaas_database_password: "{{ lookup('ansible.builtin.password', iop_vmaas_database_password_file, chars=['ascii_letters', 'digits']) }}" iop_vulnerability_database_host: "{{ iop_database_host }}" iop_vulnerability_database_port: "{{ iop_database_port }}" iop_vulnerability_database_name: vulnerability_db iop_vulnerability_database_user: vulnerability_admin -iop_vulnerability_database_password: CHANGEME +iop_vulnerability_database_password_file: "{{ obsah_state_path }}/iop-vulnerability-db-password" +iop_vulnerability_database_password: "{{ lookup('ansible.builtin.password', iop_vulnerability_database_password_file, chars=['ascii_letters', 'digits']) }}" iop_postgresql_databases: - name: "{{ iop_inventory_database_name }}" From 7fe7be074197233d5f317d812952e9f02b8a4672 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Fri, 29 May 2026 08:43:58 +0200 Subject: [PATCH 7/9] mark ca_key_password as no-qa for static secret -- it's a path --- src/vars/installer_certificates.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vars/installer_certificates.yml b/src/vars/installer_certificates.yml index 6939e6310..f94ca3cae 100644 --- a/src/vars/installer_certificates.yml +++ b/src/vars/installer_certificates.yml @@ -1,5 +1,5 @@ --- -ca_key_password: "/root/ssl-build/katello-default-ca.pwd" +ca_key_password: "/root/ssl-build/katello-default-ca.pwd" # noqa: var-secrets[no-static] ca_certificate: "/root/ssl-build/katello-default-ca.crt" ca_key: "/root/ssl-build/katello-default-ca.key" server_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" From c452cdd60e9ed1812b82d991a81736ab339d3514 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Fri, 29 May 2026 09:05:33 +0200 Subject: [PATCH 8/9] fix new lint rules in foreman_development role --- development/roles/foreman_development/defaults/main.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/development/roles/foreman_development/defaults/main.yaml b/development/roles/foreman_development/defaults/main.yaml index 6977b125b..53539bfae 100644 --- a/development/roles/foreman_development/defaults/main.yaml +++ b/development/roles/foreman_development/defaults/main.yaml @@ -12,13 +12,13 @@ foreman_development_client_certificate: "{{ foreman_client_certificate }}" foreman_development_client_key: "{{ foreman_client_key }}" foreman_development_admin_user: "admin" -foreman_development_admin_password: "changeme" +foreman_development_admin_password: "changeme" # noqa: var-secrets[no-static] foreman_development_candlepin_url: "https://localhost:23443/candlepin" foreman_development_git_repo: "https://github.com/theforeman/foreman.git" foreman_development_git_revision: "develop" -foreman_development_github_username: "" +foreman_development_github_username: "" # noqa: var-defaults[no-empty] foreman_development_hammer_git_repo: "https://github.com/theforeman/hammer-cli.git" @@ -33,7 +33,7 @@ foreman_development_database_host: "localhost" foreman_development_database_port: 5432 foreman_development_database_name: "foreman_development" foreman_development_database_user: "foreman" -foreman_development_database_password: "foreman" +foreman_development_database_password: "foreman" # noqa: var-secrets[no-static] foreman_development_nodejs_stream: "22" From 7b0048c5d7cd4821588507a9c525fe489f8c087c Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Fri, 29 May 2026 09:19:54 +0200 Subject: [PATCH 9/9] move tests to the normal location, so they get executed --- .ansible-lint-rules/conftest.py | 3 -- .ansible-lint-rules/empty_defaults.py | 40 --------------- .ansible-lint-rules/no_static_secrets.py | 52 -------------------- tests/ansible_lint/conftest.py | 29 +++++++++++ tests/ansible_lint/test_empty_defaults.py | 19 +++++++ tests/ansible_lint/test_no_static_secrets.py | 25 ++++++++++ 6 files changed, 73 insertions(+), 95 deletions(-) delete mode 100644 .ansible-lint-rules/conftest.py create mode 100644 tests/ansible_lint/conftest.py create mode 100644 tests/ansible_lint/test_empty_defaults.py create mode 100644 tests/ansible_lint/test_no_static_secrets.py diff --git a/.ansible-lint-rules/conftest.py b/.ansible-lint-rules/conftest.py deleted file mode 100644 index c186565df..000000000 --- a/.ansible-lint-rules/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Makes ansible-lint pytest fixtures available for inline rule tests.""" - -from ansiblelint.testing.fixtures import * # noqa: F403 diff --git a/.ansible-lint-rules/empty_defaults.py b/.ansible-lint-rules/empty_defaults.py index abf30b047..7b95ae4c4 100644 --- a/.ansible-lint-rules/empty_defaults.py +++ b/.ansible-lint-rules/empty_defaults.py @@ -2,7 +2,6 @@ from __future__ import annotations -import sys from typing import TYPE_CHECKING from ansiblelint.rules import AnsibleLintRule @@ -51,42 +50,3 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: ) return results - - -if "pytest" in sys.modules: - from ansiblelint.config import Options - from ansiblelint.file_utils import Lintable - from ansiblelint.rules import RulesCollection - from ansiblelint.runner import Runner - - def test_empty_defaults_are_flagged( - config_options: Options, - app: object, - ) -> None: - """Null and empty string defaults produce match errors.""" - rules = RulesCollection(app=app, options=config_options) - rules.register(EmptyDefaultsRule()) - results = Runner( - Lintable("tests/fixtures/ansible-lint/roles/test_empty_defaults"), - rules=rules, - ).run() - empty_results = [r for r in results if r.rule.id == EmptyDefaultsRule.id] - assert len(empty_results) == 4 - for result in empty_results: - assert result.tag == "var-defaults[no-empty]" - - def test_vars_file_not_checked( - config_options: Options, - app: object, - ) -> None: - """Vars files are not checked, only defaults.""" - rules = RulesCollection(app=app, options=config_options) - rules.register(EmptyDefaultsRule()) - results = Runner( - Lintable( - "tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml" - ), - rules=rules, - ).run() - empty_results = [r for r in results if r.rule.id == EmptyDefaultsRule.id] - assert len(empty_results) == 0 diff --git a/.ansible-lint-rules/no_static_secrets.py b/.ansible-lint-rules/no_static_secrets.py index b51bb30fc..80701376c 100644 --- a/.ansible-lint-rules/no_static_secrets.py +++ b/.ansible-lint-rules/no_static_secrets.py @@ -2,7 +2,6 @@ from __future__ import annotations -import sys from typing import TYPE_CHECKING from ansiblelint.rules import AnsibleLintRule @@ -62,54 +61,3 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: ) return results - - -if "pytest" in sys.modules: - from ansiblelint.config import Options - from ansiblelint.file_utils import Lintable - from ansiblelint.rules import RulesCollection - from ansiblelint.runner import Runner - - def _run_rule(path: str, config_options: Options, app: object) -> list: - rules = RulesCollection(app=app, options=config_options) - rules.register(NoStaticSecretsRule()) - results = Runner(Lintable(path), rules=rules).run() - return [r for r in results if r.rule.id == NoStaticSecretsRule.id] - - def test_static_secrets_flagged( - config_options: Options, - app: object, - ) -> None: - """Static secret values produce match errors.""" - results = _run_rule( - "tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml", - config_options, - app, - ) - assert len(results) == 2 - for result in results: - assert result.tag == "var-secrets[no-static]" - - def test_jinja_secrets_pass( - config_options: Options, - app: object, - ) -> None: - """Jinja expression secrets are not flagged.""" - results = _run_rule( - "tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml", - config_options, - app, - ) - assert len(results) == 0 - - def test_non_role_vars_checked( - config_options: Options, - app: object, - ) -> None: - """Non-role vars files are also checked.""" - results = _run_rule( - "tests/fixtures/ansible-lint/vars/secrets.yml", - config_options, - app, - ) - assert len(results) == 1 diff --git a/tests/ansible_lint/conftest.py b/tests/ansible_lint/conftest.py new file mode 100644 index 000000000..fa605ad46 --- /dev/null +++ b/tests/ansible_lint/conftest.py @@ -0,0 +1,29 @@ +"""Makes ansible-lint pytest fixtures available for lint rule tests.""" + +import pytest +from ansiblelint.file_utils import Lintable +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner +from ansiblelint.testing.fixtures import * # noqa: F403 + +CUSTOM_RULESDIR = ".ansible-lint-rules" + + +@pytest.fixture +def custom_rules(config_options, app): # noqa: F811 + """Return a RulesCollection loaded from .ansible-lint-rules/.""" + from ansiblelint.rules import RulesCollection + + return RulesCollection( + app=app, + rulesdirs=[CUSTOM_RULESDIR], + options=config_options, + ) + + +@pytest.fixture +def ansible_lint_runner(request, custom_rules: RulesCollection) -> list: + path = request.param[0] + rule_id = request.param[1] + results = Runner(Lintable(path), rules=custom_rules).run() + return [r for r in results if r.rule.id == rule_id] diff --git a/tests/ansible_lint/test_empty_defaults.py b/tests/ansible_lint/test_empty_defaults.py new file mode 100644 index 000000000..c3bca0401 --- /dev/null +++ b/tests/ansible_lint/test_empty_defaults.py @@ -0,0 +1,19 @@ +"""Tests for var-defaults[no-empty] rule.""" + +import pytest + +RULE_ID = "var-defaults" + + +@pytest.mark.parametrize("ansible_lint_runner", [("tests/fixtures/ansible-lint/roles/test_empty_defaults", RULE_ID)], indirect=True) +def test_empty_defaults_are_flagged(ansible_lint_runner) -> None: + """Null and empty string defaults produce match errors.""" + assert len(ansible_lint_runner) == 4 + for result in ansible_lint_runner: + assert result.tag == "var-defaults[no-empty]" + + +@pytest.mark.parametrize("ansible_lint_runner", [("tests/fixtures/ansible-lint/roles/test_empty_defaults/vars/main.yml", RULE_ID)], indirect=True) +def test_vars_file_not_checked(ansible_lint_runner) -> None: + """Vars files are not checked, only defaults.""" + assert len(ansible_lint_runner) == 0 diff --git a/tests/ansible_lint/test_no_static_secrets.py b/tests/ansible_lint/test_no_static_secrets.py new file mode 100644 index 000000000..df64a3452 --- /dev/null +++ b/tests/ansible_lint/test_no_static_secrets.py @@ -0,0 +1,25 @@ +"""Tests for var-secrets[no-static] rule.""" + +import pytest + +RULE_ID = "var-secrets" + + +@pytest.mark.parametrize("ansible_lint_runner", [("tests/fixtures/ansible-lint/roles/test_static_secrets/defaults/main.yml", RULE_ID)], indirect=True) +def test_static_secrets_flagged(ansible_lint_runner) -> None: + """Static secret values produce match errors.""" + assert len(ansible_lint_runner) == 2 + for result in ansible_lint_runner: + assert result.tag == "var-secrets[no-static]" + + +@pytest.mark.parametrize("ansible_lint_runner", [("tests/fixtures/ansible-lint/roles/test_static_secrets/vars/main.yml", RULE_ID)], indirect=True) +def test_jinja_secrets_pass(ansible_lint_runner) -> None: + """Jinja expression secrets are not flagged.""" + assert len(ansible_lint_runner) == 0 + + +@pytest.mark.parametrize("ansible_lint_runner", [("tests/fixtures/ansible-lint/vars/secrets.yml", RULE_ID)], indirect=True) +def test_non_role_vars_checked(ansible_lint_runner) -> None: + """Non-role vars files are also checked.""" + assert len(ansible_lint_runner) == 1