feat(auth): forged browser requests are refused (#5031)
#5031 "Forged-request protection: SameSite cookie, per-session CSRF token on every non-GET, HTMX header". A page on another site could make a logged-in browser POST to Steward with the session cookie attached. - steward/auth/csrf.py: a per-session token, checked in one before_request for every POST/PUT/PATCH/DELETE (header X-CSRF-Token or form field csrf_token, constant-time compare). The bearer-authenticated host-agent ingest is exempt; /mcp never reaches Quart. - Every POST form renders {{ csrf_field() }}; base.html carries the token in a meta tag and adds it to every HTMX request; the dashboard's fetch() calls send it. - The session cookie is SameSite=Lax; login issues a fresh token. - PUBLIC_BASE_URL is now loaded from Settings at startup (it was only set when Settings → General was saved). - tests/test_csrf.py: refusal, acceptance, rotation, the ingest exemption, and a guard that every POST form in the templates carries the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,17 +20,17 @@
|
||||
<a href="/plugins/host_agent/{{ host.id }}/" class="btn btn-sm btn-ghost">Full metrics →</a>
|
||||
{% if session.user_role == 'admin' %}
|
||||
{% if ansible_available and target %}
|
||||
<form method="post" action="/plugins/host_agent/update" style="margin:0;">
|
||||
<form method="post" action="/plugins/host_agent/update" style="margin:0;">{{ csrf_field() }}
|
||||
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
||||
<button type="submit" class="btn btn-sm" title="Refresh agent.py + restart (token preserved)">Update agent</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/rotate-token" style="margin:0;"
|
||||
onsubmit="return confirm('Rotate token? The agent stops reporting until reinstalled/updated with the new token.');">
|
||||
onsubmit="return confirm('Rotate token? The agent stops reporting until reinstalled/updated with the new token.');">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm btn-ghost">Rotate token</button>
|
||||
</form>
|
||||
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/delete" style="margin:0;"
|
||||
onsubmit="return confirm('Remove this agent registration? Metrics stop until re-registered.');">
|
||||
onsubmit="return confirm('Remove this agent registration? Metrics stop until re-registered.');">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -49,7 +49,7 @@
|
||||
Connects over a one-time bootstrap user + password (not stored).
|
||||
</p>
|
||||
<form method="post" action="/plugins/host_agent/provision"
|
||||
style="display:flex;gap:0.6rem;align-items:flex-end;flex-wrap:wrap;">
|
||||
style="display:flex;gap:0.6rem;align-items:flex-end;flex-wrap:wrap;">{{ csrf_field() }}
|
||||
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label>Bootstrap user</label>
|
||||
@@ -96,7 +96,7 @@
|
||||
border-radius:6px;padding:0.7rem 0.9rem;display:flex;align-items:center;gap:0.8rem;flex-wrap:wrap;">
|
||||
<span style="color:var(--yellow);">⚠</span>
|
||||
<span style="font-size:0.84rem;flex:1;min-width:13rem;">No managed SSH key yet — Steward needs one to log into hosts.</span>
|
||||
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">
|
||||
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">{{ csrf_field() }}
|
||||
<input type="hidden" name="next" value="/hosts/{{ host.id }}">
|
||||
<button type="submit" class="btn btn-sm">Generate managed key</button>
|
||||
</form>
|
||||
@@ -115,7 +115,7 @@
|
||||
</p>
|
||||
{# Provision: brand-new host (creates steward account + key over a one-time password) #}
|
||||
<form method="post" action="/plugins/host_agent/provision"
|
||||
style="display:flex;gap:0.6rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:0.75rem;">
|
||||
style="display:flex;gap:0.6rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:0.75rem;">{{ csrf_field() }}
|
||||
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);width:100%;">Provision (first contact — fresh host):</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
@@ -129,7 +129,7 @@
|
||||
<button type="submit" class="btn btn-sm">Provision</button>
|
||||
</form>
|
||||
{# Install: host already has the steward account (managed key works) #}
|
||||
<form method="post" action="/plugins/host_agent/deploy" style="display:flex;gap:0.6rem;align-items:center;">
|
||||
<form method="post" action="/plugins/host_agent/deploy" style="display:flex;gap:0.6rem;align-items:center;">{{ csrf_field() }}
|
||||
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
||||
<span style="font-size:0.78rem;color:var(--text-muted);">Already provisioned?</span>
|
||||
<button type="submit" class="btn btn-sm btn-ghost">Install agent (managed key)</button>
|
||||
@@ -138,7 +138,7 @@
|
||||
|
||||
{% if reg and session.user_role == 'admin' %}
|
||||
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/delete" style="margin:0.75rem 0 0;"
|
||||
onsubmit="return confirm('Clear this pending registration? Its token is discarded.');">
|
||||
onsubmit="return confirm('Clear this pending registration? Its token is discarded.');">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm btn-ghost">Clear pending registration</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/plugins/host_agent/fleet/add-host"
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">{{ csrf_field() }}
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label>Host name</label>
|
||||
<input type="text" name="name" required>
|
||||
@@ -63,7 +63,7 @@
|
||||
<span style="font-size:0.84rem;flex:1;min-width:14rem;">
|
||||
No managed SSH key yet — Steward needs one to log into hosts as <code>steward</code>.
|
||||
</span>
|
||||
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">
|
||||
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">{{ csrf_field() }}
|
||||
<input type="hidden" name="next" value="/plugins/host_agent/fleet/">
|
||||
<button type="submit" class="btn btn-sm">Generate managed key</button>
|
||||
</form>
|
||||
@@ -85,7 +85,7 @@
|
||||
agent. Run this once per host.
|
||||
</p>
|
||||
<form method="post" action="/plugins/host_agent/provision"
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">{{ csrf_field() }}
|
||||
<div class="form-group" style="margin-bottom:0;">{{ scope_select }}</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label>Bootstrap user</label>
|
||||
@@ -111,7 +111,7 @@
|
||||
mints a fresh token, and installs the agent. No <code>curl | sh</code> needed.
|
||||
</p>
|
||||
<form method="post" action="/plugins/host_agent/deploy"
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">{{ csrf_field() }}
|
||||
<div class="form-group" style="margin-bottom:0;">{{ scope_select }}</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label>Report interval (s)</label>
|
||||
@@ -129,7 +129,7 @@
|
||||
so identity is preserved.
|
||||
</p>
|
||||
<form method="post" action="/plugins/host_agent/update"
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">
|
||||
style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">{{ csrf_field() }}
|
||||
<div class="form-group" style="margin-bottom:0;">{{ scope_select }}</div>
|
||||
<button type="submit" class="btn">Update</button>
|
||||
</form>
|
||||
@@ -156,12 +156,12 @@
|
||||
<td class="td-actions">
|
||||
<form method="post" action="/plugins/host_agent/fleet/{{ item.reg.host_id }}/rotate-token"
|
||||
style="display:inline;"
|
||||
onsubmit="return confirm('Rotate the token for this host? The existing agent will stop working until reinstalled with the new token.');">
|
||||
onsubmit="return confirm('Rotate the token for this host? The existing agent will stop working until reinstalled with the new token.');">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-ghost btn-sm">Rotate token</button>
|
||||
</form>
|
||||
<form method="post" action="/plugins/host_agent/fleet/{{ item.reg.host_id }}/delete"
|
||||
style="display:inline;"
|
||||
onsubmit="return confirm('Delete this host registration? The agent will be unable to report metrics until re-registered.');">
|
||||
onsubmit="return confirm('Delete this host registration? The agent will be unable to report metrics until re-registered.');">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user