Review findings surfaced while syncing decdn/internal-devops from this template at 7a6d744
(decdn/internal-devops#15). All line references are against 7a6d744. Two were fixed downstream
in inventory/docs; the rest live in template-owned files and must be fixed here, since anything
patched in the internal instance is clobbered by the next sync.
Ordered by severity.
1. grafana_alloy_secret_file defaults into a directory the node user owns
ansible/roles/grafana_alloy/defaults/main.yml:37 defaults the Grafana Cloud credential file to
/etc/decdn/grafana-alloy.env, but ansible/roles/decdn_node/tasks/main.yml:1109 creates
/etc/decdn as decdn:decdn 0750. The file itself is root:root 0600, yet write permission on
the parent directory is enough to unlink and replace it — so a compromised (or merely buggy)
decdn process can substitute the EnvironmentFile that systemd expands as root into the
Alloy unit (templates/alloy.service.j2, EnvironmentFile=). That is a privilege boundary the
role explicitly claims to hold: the unit comment says "the agent never needs (and must not get)
direct read access", and preflight enforces root:root 0600 on the file.
Suggested fix: default to a path outside any service-owned directory, e.g.
/etc/grafana-alloy.env (it already satisfies the validate-paths.yml /etc/... regex and sits
outside grafana_alloy_config_dir, which the disable path removes wholesale). Optionally have
preflight also assert the parent directory is root-owned and not group/other writable.
Downstream workaround applied: overridden in inventory.
2. Inventory-rendered DECDN_RPC_URL is not safe under the role's own bash sourcing
ansible/roles/decdn_node/tasks/main.yml:1299 renders the value with to_json, i.e. double
quotes. ansible/roles/decdn_node/tasks/main.yml:1444-1456 then sources that file with
/bin/bash (set -a; . /etc/decdn/decdn.env) before decdn config validate, and the task's own
comment notes bash "expands $VAR and executes $(...) and backticks, which systemd would pass
through literally".
So the role's documented contract and its rendering disagree: roles/decdn_node/README.md:305-309
tells operators to single-quote any value containing $ or a backtick, and states that the
escaping applied to decdn_extra_env is not applied to decdn_rpc_url, "which is rendered
raw". An RPC URL containing a literal $ (legal in a query string / API key) is silently mangled
at validate time; a $(...) substring is executed as the decdn user.
Suggested fix: render DECDN_RPC_URL single-quoted with ' -> '\'' escaping (as for
decdn_extra_env), or source the env file in a way that does not perform expansion. Either way,
reconcile README lines 305-309 with the actual behaviour.
3. Teardown marker is matched anywhere in the unit file, not as the ownership stamp
ansible/roles/grafana_alloy/tasks/main.yml:51 gates removal on
_ga_managed_marker in (… | b64decode). vars/main.yml:4 documents the marker as "the ownership
stamp emitted as the first line" of the template. A hand-written unit that merely mentions
MANAGED BY the grafana_alloy role in a comment (e.g. an operator noting the role used to own the
service) is then treated as role-managed and deleted on the disable path.
Suggested fix: compare against the first line, e.g.
… | b64decode).split('\n')[0] is search(...) / startswith(_ga_managed_marker), matching what
the template actually emits and what vars/main.yml claims.
4. validate-paths.yml rejects .. segments but not .
ansible/roles/grafana_alloy/tasks/validate-paths.yml:16-18 refuses .. and empty segments, but a
bare . passes both the regexes (. is in the character class) and the segment checks. So
grafana_alloy_config_dir: /etc/alloy/. normalises to /etc/alloy — harmless — while something
like /var/lib/alloy/. on the destructive disable path (state: absent) resolves to a different
directory than the literal string suggests, which is exactly the class of surprise this guard
exists to prevent.
Suggested fix: add - "'.' not in item.value.split('/')" alongside the existing .. check.
5. No preflight rejects grafana_alloy_user: root
templates/alloy.service.j2 renders User={{ grafana_alloy_user }} and the surrounding comments
build the whole credential model on Alloy running unprivileged ("read by systemd as ROOT before
privileges drop, so the agent never needs … direct read access"). Nothing stops an override from
setting grafana_alloy_user: root, which silently dissolves that boundary.
Suggested fix: assert grafana_alloy_user not in ['root', '0'] (same for the group) in
tasks/preflight.yml.
6. (nit) Uncovered node.toml.j2 OTLP branch
templates/node.toml.j2:356-364 has three outcomes. Molecule covers the explicit endpoint
(molecule/default/converge.yml:52) and the Grafana-enabled fallback
(molecule/grafana-cloud/verify.yml:150-155), but nothing asserts the third: Grafana disabled
with an empty decdn_otlp_endpoint must omit observability.otlp_endpoint entirely. A one-line
assertion in an existing verify would close it.
Happy to send PRs for any subset — say which you'd like and I'll open them against main.
Review findings surfaced while syncing
decdn/internal-devopsfrom this template at7a6d744(decdn/internal-devops#15). All line references are against
7a6d744. Two were fixed downstreamin inventory/docs; the rest live in template-owned files and must be fixed here, since anything
patched in the internal instance is clobbered by the next sync.
Ordered by severity.
1.
grafana_alloy_secret_filedefaults into a directory the node user ownsansible/roles/grafana_alloy/defaults/main.yml:37defaults the Grafana Cloud credential file to/etc/decdn/grafana-alloy.env, butansible/roles/decdn_node/tasks/main.yml:1109creates/etc/decdnasdecdn:decdn 0750. The file itself isroot:root 0600, yet write permission onthe parent directory is enough to unlink and replace it — so a compromised (or merely buggy)
decdnprocess can substitute theEnvironmentFilethat systemd expands as root into theAlloy unit (
templates/alloy.service.j2,EnvironmentFile=). That is a privilege boundary therole explicitly claims to hold: the unit comment says "the agent never needs (and must not get)
direct read access", and preflight enforces
root:root 0600on the file.Suggested fix: default to a path outside any service-owned directory, e.g.
/etc/grafana-alloy.env(it already satisfies thevalidate-paths.yml/etc/...regex and sitsoutside
grafana_alloy_config_dir, which the disable path removes wholesale). Optionally havepreflight also assert the parent directory is root-owned and not group/other writable.
Downstream workaround applied: overridden in inventory.
2. Inventory-rendered
DECDN_RPC_URLis not safe under the role's own bash sourcingansible/roles/decdn_node/tasks/main.yml:1299renders the value withto_json, i.e. doublequotes.
ansible/roles/decdn_node/tasks/main.yml:1444-1456then sources that file with/bin/bash(set -a; . /etc/decdn/decdn.env) beforedecdn config validate, and the task's owncomment notes bash "expands
$VARand executes$(...)and backticks, which systemd would passthrough literally".
So the role's documented contract and its rendering disagree:
roles/decdn_node/README.md:305-309tells operators to single-quote any value containing
$or a backtick, and states that theescaping applied to
decdn_extra_envis not applied todecdn_rpc_url, "which is renderedraw". An RPC URL containing a literal
$(legal in a query string / API key) is silently mangledat validate time; a
$(...)substring is executed as thedecdnuser.Suggested fix: render
DECDN_RPC_URLsingle-quoted with'->'\''escaping (as fordecdn_extra_env), or source the env file in a way that does not perform expansion. Either way,reconcile README lines 305-309 with the actual behaviour.
3. Teardown marker is matched anywhere in the unit file, not as the ownership stamp
ansible/roles/grafana_alloy/tasks/main.yml:51gates removal on_ga_managed_marker in (… | b64decode).vars/main.yml:4documents the marker as "the ownershipstamp emitted as the first line" of the template. A hand-written unit that merely mentions
MANAGED BY the grafana_alloy rolein a comment (e.g. an operator noting the role used to own theservice) is then treated as role-managed and deleted on the disable path.
Suggested fix: compare against the first line, e.g.
… | b64decode).split('\n')[0] is search(...)/startswith(_ga_managed_marker), matching whatthe template actually emits and what
vars/main.ymlclaims.4.
validate-paths.ymlrejects..segments but not.ansible/roles/grafana_alloy/tasks/validate-paths.yml:16-18refuses..and empty segments, but abare
.passes both the regexes (.is in the character class) and the segment checks. Sografana_alloy_config_dir: /etc/alloy/.normalises to/etc/alloy— harmless — while somethinglike
/var/lib/alloy/.on the destructive disable path (state: absent) resolves to a differentdirectory than the literal string suggests, which is exactly the class of surprise this guard
exists to prevent.
Suggested fix: add
- "'.' not in item.value.split('/')"alongside the existing..check.5. No preflight rejects
grafana_alloy_user: roottemplates/alloy.service.j2rendersUser={{ grafana_alloy_user }}and the surrounding commentsbuild the whole credential model on Alloy running unprivileged ("read by systemd as ROOT before
privileges drop, so the agent never needs … direct read access"). Nothing stops an override from
setting
grafana_alloy_user: root, which silently dissolves that boundary.Suggested fix: assert
grafana_alloy_user not in ['root', '0'](same for the group) intasks/preflight.yml.6. (nit) Uncovered
node.toml.j2OTLP branchtemplates/node.toml.j2:356-364has three outcomes. Molecule covers the explicit endpoint(
molecule/default/converge.yml:52) and the Grafana-enabled fallback(
molecule/grafana-cloud/verify.yml:150-155), but nothing asserts the third: Grafana disabledwith an empty
decdn_otlp_endpointmust omitobservability.otlp_endpointentirely. A one-lineassertion in an existing verify would close it.
Happy to send PRs for any subset — say which you'd like and I'll open them against
main.