GL-7 Part 1: passphrase hardening on the customer page (security)

The Setup Command panel rendered the per-customer retrieval passphrase (the
secret that fetches the WHOLE config) in cleartext twice — as #retrieval-pw
text and baked into the Option-3 curl's X-Retrieval-Password header — which
contradicts the panel's own "never on the command line" guidance. Now:
the retrieval password is MASKED by default (bullet run) with Reveal/Hide +
copy-secret controls (value lives in data-secret — the existing reveal
model); the Option-3 debug command carries a <YOUR-RETRIEVAL-PASSWORD>
placeholder, never the secret. Render test asserts the secret is not baked
into any command + is masked by default; red-proof (bake it back) FAILS.
(A zero-secret-in-DOM reveal-on-demand fetch is a noted follow-up, not this task.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 08:23:41 +02:00
parent fb8e5111cf
commit b93dd03f0a
2 changed files with 93 additions and 4 deletions
@@ -360,9 +360,11 @@
<div>
<span class="label">Retrieval Password</span>
<div class="credential-box">
<code id="retrieval-pw">{{.Config.RetrievalPassword}}</code>
<button type="button" class="copy-btn" onclick="copyText('retrieval-pw')" title="Copy">&#x2398;</button>
<code id="retrieval-pw" data-secret="{{.Config.RetrievalPassword}}">&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;</code>
<button type="button" class="copy-btn" id="reveal-retrieval-pw" onclick="toggleSecret('retrieval-pw')" title="Reveal / hide">Reveal</button>
<button type="button" class="copy-btn" onclick="copySecret('retrieval-pw')" title="Copy">&#x2398;</button>
</div>
<span class="form-hint">The per-customer secret that fetches the whole config — masked by default; never place it on a command line (the installer reads it at a no-echo prompt).</span>
</div>
<form method="POST" action="/configs/{{.CustomerID}}/regen-password" style="margin-top: 0.5rem;"
onsubmit="return confirm('Regenerate retrieval password? The old password will stop working immediately.')">
@@ -410,9 +412,9 @@
</div>
<h3 style="margin-top: 1rem;">Option 3: Manual config fetch (debug only)</h3>
<p class="text-muted" style="margin: 0 0 0.4rem; font-size: 0.8rem;">The same payload the controller pulls itself — for inspection, not normal provisioning.</p>
<p class="text-muted" style="margin: 0 0 0.4rem; font-size: 0.8rem;">The same payload the controller pulls itself — for inspection, not normal provisioning. Replace the placeholder with the Retrieval Password above (Reveal to see it) — it is intentionally NOT baked into this command.</p>
<div class="credential-box">
<code id="cmd-curl">curl -fsSL https://hub.felhom.eu/api/v1/config/{{.CustomerID}} -H "X-Retrieval-Password: {{.Config.RetrievalPassword}}" -o controller.yaml</code>
<code id="cmd-curl">curl -fsSL https://hub.felhom.eu/api/v1/config/{{.CustomerID}} -H "X-Retrieval-Password: &lt;YOUR-RETRIEVAL-PASSWORD&gt;" -o controller.yaml</code>
<button type="button" class="copy-btn" onclick="copyText('cmd-curl')" title="Copy">&#x2398;</button>
</div>
</section>
@@ -694,6 +696,32 @@
});
}
// Masked-secret controls (retrieval password): the value lives in data-secret and is masked in
// the visible node by default; Reveal toggles it, Copy copies the real value. It is never baked
// into a copyable command (see the Option-3 placeholder).
var _secretMask = '•'.repeat(16);
function toggleSecret(elementId) {
var el = document.getElementById(elementId);
var btn = document.getElementById('reveal-' + elementId);
if (el.dataset.revealed === '1') {
el.textContent = _secretMask; el.dataset.revealed = '0';
if (btn) btn.textContent = 'Reveal';
} else {
el.textContent = el.getAttribute('data-secret') || ''; el.dataset.revealed = '1';
if (btn) btn.textContent = 'Hide';
}
}
function copySecret(elementId) {
var el = document.getElementById(elementId);
var val = (el.getAttribute('data-secret') || '').trim();
navigator.clipboard.writeText(val).then(function() {
var btn = el.parentElement.querySelectorAll('.copy-btn');
var b = btn[btn.length - 1]; var orig = b.innerHTML;
b.innerHTML = '&#x2713;';
setTimeout(function() { b.innerHTML = orig; }, 1500);
});
}
function disableGeo(customerID) {
if (!confirm('Összes geo-korlátozás eltávolítása?\n\nEz közvetlenül törli a Cloudflare WAF szabályokat és értesíti a controllert.')) return;
var btn = document.getElementById('btn-geo-disable');