mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 03:15:56 +00:00
fix(roles): keep credentials out of the argv list form too
`container exec` was handed 25 live secrets across 10 roles through an
`argv:` list, where `-e` sits on one entry and `NAME={{ secret }}` on the
next. Each value is an argv element of the host-side process, so `ps` shows
it to every local account while the call runs, and `no_log` masks Ansible's
output rather than the process table. All 25 now travel as `-e NAME` with
the value in the task's `environment:`.
The three container lints added earlier this week all missed this, because
they scan line by line and the two halves of the flag never share a line.
`container-exec-env-value` therefore reads `argv:` lists out of the parsed
YAML as well.
Widening it exposed the name heuristic as too blunt: `_TOKEN_NAME`,
`_API_KEY_NAME` and `_LIFETIME_DAYS` match `TOKEN` and `KEY` while carrying
nothing secret. Nine `nocheck` markers would have papered over that, so the
rule now disqualifies a name whose last segment describes a credential
instead of holding one. A secret reached through such a name is still caught
by its source expression.
Counting by variable name hid one site: discourse carries the OIDC client
secret in three tasks, and deduplicating by name collapsed them to one. The
sweep is verified by the raw occurrence count instead, which is why this is
25 rather than the 24 first reported.
Roles touched: bigbluebutton, checkmk, discourse, gitlab, jellyfin, n8n,
nextcloud, openproject, openwebui, snipe-it.
With `command` and `shell` the value still appears once on the module
wrapper's line, which `Shell.env_prefix` builds, so the exposure shrinks to
a single process rather than vanishing.
feature/designer fixes the same bigbluebutton defect in 90d491b5a7 and goes
further: it drops the base64 encoding, which only ever existed to survive
argv quoting and is pointless once the value skips the shell, and it repairs
an administrator created unverified. That side wins at the merge, except for
its `lookup('users', 'administrator')`, which this branch forbids outside
user-administrator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6c46868e9d
commit
463053e061
15 files changed
+162
-30
No files matched your search
@@ -26,12 +26,14 @@
|
||||
- -e
|
||||
- "NEW_EMAIL={{ lookup('users', 'platform_administrator').email }}"
|
||||
- -e
|
||||
- "NEW_PASSWORD_B64={{ lookup('users', 'platform_administrator').password | b64encode }}"
|
||||
- NEW_PASSWORD_B64
|
||||
- "{{ BBB_GREENLIGHT_CONTAINER_ADDRESS | quote }}" # nocheck: container-address-quote, container-address-needs-shell compose_mode_force role, so the lookup can only yield a bare name
|
||||
- bash
|
||||
- -lc
|
||||
- "bundle exec rails runner -"
|
||||
stdin: "{{ lookup('ansible.builtin.file', role_path ~ '/files/ruby/greenlight_ensure_admin.rb') }}"
|
||||
environment:
|
||||
NEW_PASSWORD_B64: "{{ lookup('users', 'platform_administrator').password | b64encode }}"
|
||||
register: greenlight_admin_map
|
||||
changed_when: "'CHANGED' in greenlight_admin_map.stdout"
|
||||
failed_when: greenlight_admin_map.rc != 0
|
||||
|
||||
@@ -23,17 +23,20 @@
|
||||
- -e
|
||||
- "CHECKMK_ADMIN_USER=cmkadmin"
|
||||
- -e
|
||||
- "CHECKMK_ADMIN_PASSWORD={{ CHECKMK_ADMIN_PASSWORD }}"
|
||||
- CHECKMK_ADMIN_PASSWORD
|
||||
- -e
|
||||
- "CHECKMK_SITE={{ CHECKMK_SITE }}"
|
||||
- -e
|
||||
- "MCP_USER={{ CHECKMK_MCP_UPSTREAM_USER }}"
|
||||
- -e
|
||||
- "MCP_SECRET={{ CHECKMK_MCP_UPSTREAM_KEY }}"
|
||||
- MCP_SECRET
|
||||
- "{{ CHECKMK_IDENTITY_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/provision_mcp_identity.py') }}"
|
||||
environment:
|
||||
CHECKMK_ADMIN_PASSWORD: "{{ CHECKMK_ADMIN_PASSWORD }}"
|
||||
MCP_SECRET: "{{ CHECKMK_MCP_UPSTREAM_KEY }}"
|
||||
register: checkmk_mcp_identity
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
- -e
|
||||
- "DISCOURSE_OIDC_CLIENT_ID={{ OIDC.CLIENT.ID if DISCOURSE_OIDC_ENABLED | bool else '' }}"
|
||||
- -e
|
||||
- "DISCOURSE_OIDC_CLIENT_SECRET={{ OIDC.CLIENT.SECRET if DISCOURSE_OIDC_ENABLED | bool else '' }}"
|
||||
- DISCOURSE_OIDC_CLIENT_SECRET
|
||||
- -e
|
||||
- "DISCOURSE_OIDC_LOGOUT_REDIRECT={{ DISCOURSE_OIDC_LOGOUT_REDIRECT_URL if DISCOURSE_OIDC_ENABLED | bool else '' }}"
|
||||
- "{{ DISCOURSE_CONTAINER }}" # nocheck: container-exec-resolver the launcher names the container itself, so the bare name resolves in both modes
|
||||
- rails
|
||||
- r
|
||||
- "{{ lookup('file', 'ruby/apply_settings.rb') }}"
|
||||
environment:
|
||||
DISCOURSE_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET if DISCOURSE_OIDC_ENABLED | bool else '' }}"
|
||||
changed_when: false
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
@@ -41,11 +43,13 @@
|
||||
- -e
|
||||
- "DISCOURSE_ADMIN_USERNAME={{ lookup('users', 'platform_administrator').username }}"
|
||||
- -e
|
||||
- "DISCOURSE_ADMIN_PASSWORD={{ lookup('users', 'platform_administrator').password }}"
|
||||
- DISCOURSE_ADMIN_PASSWORD
|
||||
- "{{ DISCOURSE_CONTAINER }}" # nocheck: container-exec-resolver the launcher names the container itself, so the bare name resolves in both modes
|
||||
- rails
|
||||
- r
|
||||
- "{{ lookup('file', 'ruby/ensure_admin_user.rb') }}"
|
||||
environment:
|
||||
DISCOURSE_ADMIN_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
|
||||
changed_when: false
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
@@ -58,15 +62,18 @@
|
||||
- -e
|
||||
- "DISCOURSE_ADMIN_EMAIL={{ lookup('users', 'platform_administrator').email }}"
|
||||
- -e
|
||||
- "DISCOURSE_ADMIN_PASSWORD={{ lookup('users', 'platform_administrator').password }}"
|
||||
- DISCOURSE_ADMIN_PASSWORD
|
||||
- -e
|
||||
- "DISCOURSE_OIDC_ENABLED={{ DISCOURSE_OIDC_ENABLED | bool | string | lower }}"
|
||||
- -e
|
||||
- "DISCOURSE_OIDC_CLIENT_SECRET={{ OIDC.CLIENT.SECRET if DISCOURSE_OIDC_ENABLED | bool else '' }}"
|
||||
- DISCOURSE_OIDC_CLIENT_SECRET
|
||||
- "{{ DISCOURSE_CONTAINER }}" # nocheck: container-exec-resolver the launcher names the container itself, so the bare name resolves in both modes
|
||||
- rails
|
||||
- r
|
||||
- "{{ lookup('file', 'ruby/verify_settings.rb') }}"
|
||||
environment:
|
||||
DISCOURSE_ADMIN_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
|
||||
DISCOURSE_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET if DISCOURSE_OIDC_ENABLED | bool else '' }}"
|
||||
register: discourse_settings_readback
|
||||
changed_when: false
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
- -e
|
||||
- "GITLAB_MCP_PROTOCOL_VERSION={{ GITLAB_MCP_PROTOCOL_VERSION }}"
|
||||
- -e
|
||||
- "GITLAB_MCP_TOKEN={{ gitlab_mcp_token }}"
|
||||
- GITLAB_MCP_TOKEN
|
||||
- "{{ GITLAB_MCP_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- ruby
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'ruby/probe_mcp_token.rb') }}"
|
||||
environment:
|
||||
GITLAB_MCP_TOKEN: "{{ gitlab_mcp_token }}"
|
||||
register: gitlab_mcp_probe
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- -e
|
||||
- "GITLAB_MCP_TOKEN_OWNER_EMAIL={{ GITLAB_MCP_TOKEN_OWNER_EMAIL }}"
|
||||
- -e
|
||||
- "GITLAB_MCP_TOKEN_OWNER_PASSWORD={{ GITLAB_MCP_TOKEN_OWNER_PASSWORD }}"
|
||||
- GITLAB_MCP_TOKEN_OWNER_PASSWORD
|
||||
- -e
|
||||
- "GITLAB_MCP_TOKEN_LIFETIME_DAYS={{ GITLAB_MCP_TOKEN_LIFETIME_DAYS }}"
|
||||
- "{{ GITLAB_MCP_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
@@ -22,6 +22,8 @@
|
||||
- runner
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'ruby/provision_mcp_token.rb') }}"
|
||||
environment:
|
||||
GITLAB_MCP_TOKEN_OWNER_PASSWORD: "{{ GITLAB_MCP_TOKEN_OWNER_PASSWORD }}"
|
||||
register: gitlab_mcp_mint
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -28,19 +28,23 @@
|
||||
- -e
|
||||
- "JELLYFIN_ADMIN_USER={{ JELLYFIN_ADMIN_USERNAME }}"
|
||||
- -e
|
||||
- "JELLYFIN_ADMIN_PASSWORD={{ JELLYFIN_ADMIN_PASSWORD }}"
|
||||
- JELLYFIN_ADMIN_PASSWORD
|
||||
- -e
|
||||
- "MCP_USER={{ JELLYFIN_MCP_UPSTREAM_USER }}"
|
||||
- -e
|
||||
- "MCP_PASSWORD={{ JELLYFIN_MCP_UPSTREAM_PASSWORD }}"
|
||||
- MCP_PASSWORD
|
||||
- -e
|
||||
- "MCP_DEVICE_ID={{ JELLYFIN_MCP_UPSTREAM_DEVICE_ID }}"
|
||||
- -e
|
||||
- "MCP_CURRENT_TOKEN={{ MCP_UPSTREAM_CURRENT_KEY }}"
|
||||
- MCP_CURRENT_TOKEN
|
||||
- "{{ JELLYFIN_IDENTITY_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/provision_mcp_identity.py') }}"
|
||||
environment:
|
||||
JELLYFIN_ADMIN_PASSWORD: "{{ JELLYFIN_ADMIN_PASSWORD }}"
|
||||
MCP_PASSWORD: "{{ JELLYFIN_MCP_UPSTREAM_PASSWORD }}"
|
||||
MCP_CURRENT_TOKEN: "{{ MCP_UPSTREAM_CURRENT_KEY }}"
|
||||
register: jellyfin_mcp_identity
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -79,11 +79,13 @@
|
||||
- -e
|
||||
- "N8N_OWNER_EMAIL={{ N8N_OWNER_EMAIL }}"
|
||||
- -e
|
||||
- "N8N_OWNER_PASSWORD={{ N8N_OWNER_PASSWORD }}"
|
||||
- N8N_OWNER_PASSWORD
|
||||
- "{{ N8N_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- node
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'javascript/reconcile/owner_password.js') }}"
|
||||
environment:
|
||||
N8N_OWNER_PASSWORD: "{{ N8N_OWNER_PASSWORD }}"
|
||||
register: n8n_owner_password
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- -e
|
||||
- "NC_CLIENT_NAME={{ nextcloud_crm_client_name }}"
|
||||
- -e
|
||||
- "NC_CLIENT_SECRET={{ nextcloud_crm_client_secret }}"
|
||||
- NC_CLIENT_SECRET
|
||||
- "{{ nextcloud_crm_container }}" # nocheck: container-exec-resolver runs only behind the bare-name partner probe gate, compose co-location
|
||||
- php
|
||||
- -d
|
||||
@@ -23,6 +23,8 @@
|
||||
- -
|
||||
chdir: "{{ nextcloud_crm_instance_dir }}"
|
||||
stdin: "{{ lookup('file', 'php/addons/suitecrm/provision_oauth_password_client.php') }}"
|
||||
environment:
|
||||
NC_CLIENT_SECRET: "{{ nextcloud_crm_client_secret }}"
|
||||
register: nextcloud_crm_provision
|
||||
changed_when: false
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- -u
|
||||
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
||||
- -e
|
||||
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
||||
- OC_PASS
|
||||
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
||||
@@ -32,6 +32,8 @@
|
||||
- --password-from-env
|
||||
- "--display-name={{ NEXTCLOUD_MCP_USER_DISPLAY_NAME }}"
|
||||
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
||||
environment:
|
||||
OC_PASS: "{{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
||||
when: NEXTCLOUD_MCP_USERNAME not in (nextcloud_mcp_accounts.stdout | from_json)
|
||||
changed_when: true
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
@@ -46,13 +48,15 @@
|
||||
- -u
|
||||
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
||||
- -e
|
||||
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
||||
- OC_PASS
|
||||
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
||||
- user:resetpassword
|
||||
- --password-from-env
|
||||
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
||||
environment:
|
||||
OC_PASS: "{{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
||||
changed_when: true
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
|
||||
@@ -107,7 +111,7 @@
|
||||
- -u
|
||||
- "{{ NEXTCLOUD_DOCKER_USER }}"
|
||||
- -e
|
||||
- "NC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
||||
- NC_PASS
|
||||
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
|
||||
@@ -115,6 +119,8 @@
|
||||
- "{{ NEXTCLOUD_MCP_USERNAME }}"
|
||||
- --password-from-env
|
||||
- "--name={{ NEXTCLOUD_MCP_TOKEN_NAME }}"
|
||||
environment:
|
||||
NC_PASS: "{{ NEXTCLOUD_MCP_USER_PASSWORD }}"
|
||||
register: nextcloud_mcp_token_mint
|
||||
changed_when: true
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- exec
|
||||
- -i
|
||||
- -e
|
||||
- "OPENPROJECT_ADMIN_PASSWORD={{ OPENPROJECT_ADMINISTRATOR_PASSWORD }}"
|
||||
- OPENPROJECT_ADMIN_PASSWORD
|
||||
- -e
|
||||
- "OPENPROJECT_ADMIN_LOGIN={{ OPENPROJECT_ADMINISTRATOR_USERNAME }}"
|
||||
- -e
|
||||
@@ -23,6 +23,8 @@
|
||||
- -lc
|
||||
- "cd /app && bundle exec rails runner -"
|
||||
stdin: "{{ lookup('file', 'ruby/ensure_admin_user.rb') }}"
|
||||
environment:
|
||||
OPENPROJECT_ADMIN_PASSWORD: "{{ OPENPROJECT_ADMINISTRATOR_PASSWORD }}"
|
||||
delegate_to: "{{ OPENPROJECT_WEB_HOST_NODE }}"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
|
||||
@@ -10,18 +10,22 @@
|
||||
- -e
|
||||
- "OPENPROJECT_GITLAB_BOT_MAIL={{ OPENPROJECT_GITLAB_BOT.email }}"
|
||||
- -e
|
||||
- "OPENPROJECT_GITLAB_BOT_PASSWORD={{ OPENPROJECT_GITLAB_BOT.password }}"
|
||||
- OPENPROJECT_GITLAB_BOT_PASSWORD
|
||||
- -e
|
||||
- "OPENPROJECT_GITLAB_TOKEN_NAME={{ openproject_addon.value.config.token_name }}"
|
||||
- -e
|
||||
- "OPENPROJECT_GITLAB_WEBHOOK_SECRET={{ OPENPROJECT_GITLAB_WEBHOOK_SECRET }}"
|
||||
- OPENPROJECT_GITLAB_WEBHOOK_SECRET
|
||||
- -e
|
||||
- "OPENPROJECT_GITLAB_API_TOKEN={{ OPENPROJECT_GITLAB_TOKEN_HELD }}"
|
||||
- OPENPROJECT_GITLAB_API_TOKEN
|
||||
- "{{ OPENPROJECT_WEB_LOCAL_CID }}" # nocheck: container-exec-resolver CID resolved at delegate_to runtime; container_address targets a manager-only subshell
|
||||
- bash
|
||||
- -lc
|
||||
- "cd /app && bundle exec rails runner -"
|
||||
stdin: "{{ lookup('file', 'ruby/addons/gitlab/ensure_gitlab_webhook.rb') }}"
|
||||
environment:
|
||||
OPENPROJECT_GITLAB_BOT_PASSWORD: "{{ OPENPROJECT_GITLAB_BOT.password }}"
|
||||
OPENPROJECT_GITLAB_WEBHOOK_SECRET: "{{ OPENPROJECT_GITLAB_WEBHOOK_SECRET }}"
|
||||
OPENPROJECT_GITLAB_API_TOKEN: "{{ OPENPROJECT_GITLAB_TOKEN_HELD }}"
|
||||
delegate_to: "{{ OPENPROJECT_WEB_HOST_NODE }}"
|
||||
register: openproject_gitlab_provision
|
||||
changed_when: "'CHANGED' in openproject_gitlab_provision.stdout"
|
||||
|
||||
@@ -19,17 +19,20 @@
|
||||
- -e
|
||||
- "OPENWEBUI_ADMIN_NAME={{ OPENWEBUI_API_BOT_USER.username }}"
|
||||
- -e
|
||||
- "OPENWEBUI_ADMIN_PASSWORD={{ OPENWEBUI_API_BOT_USER.password }}"
|
||||
- OPENWEBUI_ADMIN_PASSWORD
|
||||
- -e
|
||||
- "OPENWEBUI_GATEWAY_URL={{ LITELLM_BASE_LOCAL_URL if (OPENWEBUI_LITELLM_ENABLED | bool) else '' }}"
|
||||
- -e
|
||||
- "OPENWEBUI_GATEWAY_KEY={{ OPENWEBUI_LITELLM_API_KEY if (OPENWEBUI_LITELLM_ENABLED | bool) else '' }}"
|
||||
- OPENWEBUI_GATEWAY_KEY
|
||||
- -e
|
||||
- "OPENWEBUI_AGENT_MODELS={{ OPENWEBUI_AGENT_MODELS | to_json }}"
|
||||
- "{{ OPENWEBUI_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/agent_model_grants.py') }}"
|
||||
environment:
|
||||
OPENWEBUI_ADMIN_PASSWORD: "{{ OPENWEBUI_API_BOT_USER.password }}"
|
||||
OPENWEBUI_GATEWAY_KEY: "{{ OPENWEBUI_LITELLM_API_KEY if (OPENWEBUI_LITELLM_ENABLED | bool) else '' }}"
|
||||
register: openwebui_model_grants
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- -e
|
||||
- "OPENWEBUI_ADMIN_NAME={{ OPENWEBUI_API_BOT_USER.username }}"
|
||||
- -e
|
||||
- "OPENWEBUI_ADMIN_PASSWORD={{ OPENWEBUI_API_BOT_USER.password }}"
|
||||
- OPENWEBUI_ADMIN_PASSWORD
|
||||
- -e
|
||||
- "OPENWEBUI_MCP_GROUPS={{ openwebui_mcp_groups | to_json }}"
|
||||
- -e
|
||||
@@ -23,6 +23,8 @@
|
||||
- python3
|
||||
- "-"
|
||||
stdin: "{{ lookup('file', 'python/provision_mcp_grants.py') }}"
|
||||
environment:
|
||||
OPENWEBUI_ADMIN_PASSWORD: "{{ OPENWEBUI_API_BOT_USER.password }}"
|
||||
register: openwebui_mcp_grants
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -29,18 +29,22 @@
|
||||
- -u
|
||||
- apache
|
||||
- -e
|
||||
- "APP_KEY={{ SNIPE_IT_APP_KEY }}"
|
||||
- APP_KEY
|
||||
- -e
|
||||
- "MCP_USER={{ SNIPEIT_MCP_UPSTREAM_USER }}"
|
||||
- -e
|
||||
- "MCP_PASSWORD={{ SNIPEIT_MCP_UPSTREAM_PASSWORD }}"
|
||||
- MCP_PASSWORD
|
||||
- -e
|
||||
- "MCP_TOKEN_NAME={{ SNIPEIT_MCP_UPSTREAM_TOKEN_NAME }}"
|
||||
- -e
|
||||
- "MCP_CURRENT_TOKEN={{ MCP_UPSTREAM_CURRENT_KEY }}"
|
||||
- MCP_CURRENT_TOKEN
|
||||
- "{{ SNIPEIT_IDENTITY_LOCAL_CID }}" # nocheck: container-exec-resolver
|
||||
- php
|
||||
stdin: "{{ lookup('file', 'php/provision_mcp_identity.php') }}"
|
||||
environment:
|
||||
APP_KEY: "{{ SNIPE_IT_APP_KEY }}"
|
||||
MCP_PASSWORD: "{{ SNIPEIT_MCP_UPSTREAM_PASSWORD }}"
|
||||
MCP_CURRENT_TOKEN: "{{ MCP_UPSTREAM_CURRENT_KEY }}"
|
||||
register: snipeit_mcp_identity
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
||||
@@ -21,6 +21,10 @@ The check is on the flag, not on the command before it: associating an ``-e``
|
||||
with its ``container exec`` is unreliable across a YAML block scalar, and
|
||||
``docker run -e`` leaks the same way.
|
||||
|
||||
An ``argv:`` list spells the same thing across two entries, ``-e`` on one and
|
||||
``NAME={{ … }}`` on the next, so a line-oriented scan never sees them
|
||||
together. Those lists are therefore read from the parsed YAML as well.
|
||||
|
||||
A literal value cannot carry a secret, so ``-e RAILS_ENV=production`` passes.
|
||||
Of the interpolated ones the rule takes those carrying a credential, by the
|
||||
variable's name **or** by a credential source in the expression behind it.
|
||||
@@ -44,6 +48,7 @@ from pathlib import Path
|
||||
|
||||
from utils.annotations.suppress import is_suppressed_at
|
||||
from utils.cache.files import iter_project_files_with_content
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
@@ -62,10 +67,62 @@ _SECRET_SOURCE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
_NOT_A_SECRET_NAME = re.compile(
|
||||
r"(?:_NAME|_EMAIL|_USER|_USERNAME|_OWNER|_ID|_URL|_HOST|_PORT|_PATH|_DAYS)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_ARGV_ASSIGNMENT = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$", re.DOTALL)
|
||||
_MODULE_KEYS = (
|
||||
"command",
|
||||
"ansible.builtin.command",
|
||||
"shell",
|
||||
"ansible.builtin.shell",
|
||||
)
|
||||
|
||||
|
||||
def _is_scan_target(rel_path: str) -> bool:
|
||||
return rel_path.startswith("roles/") and rel_path.endswith(_EXTENSIONS)
|
||||
|
||||
|
||||
def _carries_credential(name: str, value: str) -> bool:
|
||||
"""Decide whether an ``NAME=VALUE`` pair hands over a secret.
|
||||
|
||||
Args:
|
||||
name: the variable name to the left of the ``=``.
|
||||
value: everything to its right, Jinja included.
|
||||
|
||||
Returns:
|
||||
True when the name reads like a credential or the expression draws
|
||||
from a credential source. A name whose last segment is ``_NAME``,
|
||||
``_OWNER``, ``_EMAIL`` or similar describes the credential rather
|
||||
than carrying it, so the name alone does not convict it; a
|
||||
credential source in the value still does.
|
||||
"""
|
||||
by_name = bool(_SECRET_NAME.search(name)) and not _NOT_A_SECRET_NAME.search(name)
|
||||
return by_name or bool(_SECRET_SOURCE.search(value))
|
||||
|
||||
|
||||
def _argv_lists(node):
|
||||
"""Yield every ``argv:`` list a task hands to a command module.
|
||||
|
||||
Args:
|
||||
node: a parsed YAML document, or any fragment of one.
|
||||
|
||||
Yields:
|
||||
Each ``argv`` list found under a ``command``/``shell`` mapping.
|
||||
"""
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
if key in _MODULE_KEYS and isinstance(value, dict):
|
||||
argv = value.get("argv")
|
||||
if isinstance(argv, list):
|
||||
yield argv
|
||||
yield from _argv_lists(value)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
yield from _argv_lists(item)
|
||||
|
||||
|
||||
class TestContainerExecEnvPassesNoValue(unittest.TestCase):
|
||||
def test_container_exec_env_flags_carry_no_value(self) -> None:
|
||||
findings: list[tuple[str, int, str]] = []
|
||||
@@ -87,15 +144,45 @@ class TestContainerExecEnvPassesNoValue(unittest.TestCase):
|
||||
value = line[match.end() : stop]
|
||||
if not _INTERPOLATED.search(value):
|
||||
continue
|
||||
if not (
|
||||
_SECRET_NAME.search(name) or _SECRET_SOURCE.search(value)
|
||||
):
|
||||
if not _carries_credential(name, value):
|
||||
continue
|
||||
line_no = idx + 1
|
||||
if is_suppressed_at(lines, line_no, _RULE, mode="same-or-above"):
|
||||
continue
|
||||
findings.append((rel, line_no, f"-e {name}={value.strip()[:60]}"))
|
||||
|
||||
if not rel.endswith((".yml", ".yaml")):
|
||||
continue
|
||||
try:
|
||||
doc = load_yaml_any(path_str)
|
||||
except Exception: # pragma: no cover - malformed YAML is another rule's
|
||||
continue
|
||||
for argv in _argv_lists(doc):
|
||||
for element in argv:
|
||||
if not isinstance(element, str):
|
||||
continue
|
||||
assignment = _ARGV_ASSIGNMENT.match(element)
|
||||
if not assignment:
|
||||
continue
|
||||
name, value = assignment.group(1), assignment.group(2)
|
||||
if not _INTERPOLATED.search(value):
|
||||
continue
|
||||
if not _carries_credential(name, value):
|
||||
continue
|
||||
line_no = next(
|
||||
(
|
||||
number
|
||||
for number, line in enumerate(lines, 1)
|
||||
if f"{name}=" in line
|
||||
),
|
||||
1,
|
||||
)
|
||||
if is_suppressed_at(lines, line_no, _RULE, mode="same-or-above"):
|
||||
continue
|
||||
findings.append(
|
||||
(rel, line_no, f"argv {name}={value.strip()[:52]}")
|
||||
)
|
||||
|
||||
if findings:
|
||||
formatted = "\n".join(
|
||||
f"- {p}:{ln}: {snip}"
|
||||
|
||||
Reference in new issue
Block a user