Make an admin's refusal stick, and let a revoke actually deny
Three holes in the enrollment path, all of the same shape: a decision the admin made did not survive contact with a PC that simply asked again. REJECTION WAS UNDONE BY A NEW NONCE. A rejected request re-armed itself as a fresh cycle when the PC came back with a different nonce, and with the shared key and auto-enroll on it was then issued the credential the admin had just refused. Rejection now binds to the endpoint identity - hostname plus BIOS serial - not to the nonce, so the PC stays refused however often it reinstalls. An admin clears it deliberately, through the approve path that already exists; a genuinely different PC is untouched. THE PENDING QUEUE COULD BE FILLED UNTIL IT REFUSED EVERY NEW PC. /enroll takes no auth by design - a bay that has never spoken to the server has nothing to authenticate with - so anything that can reach the server could sit at the cap and lock out real enrollments fleet-wide. Rows nobody has touched for seven days now age out before the cap refuses anyone, and the count of what went is audited. Per-identity dedup and the cap itself were already there and are left alone. REVOKE DID NOT STOP A LIVE AGENT. It fell back to the shared key and the server admitted it again, so revoking a compromised PC changed nothing while that key still worked. plugin.endpoints.revokeddeniessharedkey makes a revoked host refused on the shared key too. That switch DEFAULTS OFF, and that is the important part. Turning it on would silence any bay revoked for a key rotation, and would break the documented revoke-then-rollback path to an older agent - on an air-gapped site, mid migration, that is a fleet outage. A never-enrolled bay is never affected either way: with no credential row at all the shared key still works, which is the whole un-migrated fleet. The checkbox sits next to the other two enrollment switches and says what turning it on costs, because a setting that can stop the floor should not be reachable only through the API.
This commit is contained in:
@@ -229,6 +229,27 @@
|
||||
on record matches. Anything else waits below for an admin.
|
||||
</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle-revokeddenies"
|
||||
:checked="enrollmentSettings.revokeddeniessharedkey"
|
||||
:disabled="!isadmin || busy"
|
||||
@change="onRevokedDeniesToggle($event)"
|
||||
/>
|
||||
<span>
|
||||
<strong>A revoked PC loses the shared key too.</strong> (setting
|
||||
<code>plugin.endpoints.revokeddeniessharedkey</code>) Off by
|
||||
default, which is what the fleet does today: a PC whose credential
|
||||
was revoked falls back to the shared key and keeps reporting, so a
|
||||
credential rotation does not strand it. Turn this on and a revoke
|
||||
becomes an eviction - that PC reports nothing and receives no
|
||||
commands until it enrolls again - and the documented rollback of
|
||||
revoke plus block re-enrollment onto an older agent that only knows
|
||||
the shared key stops working. Only safe once every PC here is
|
||||
enrolled.
|
||||
</span>
|
||||
</label>
|
||||
<label class="number-row">
|
||||
<span>
|
||||
<strong>Pending request limit.</strong> How many requests may wait
|
||||
@@ -670,6 +691,9 @@ const DEFAULT_PENDING_CAP = 500
|
||||
const enrollmentSettings = ref({
|
||||
sharedkeyallowed: true,
|
||||
autoenroll: true,
|
||||
// Matches the seeded default (plugin.endpoints.revokeddeniessharedkey is
|
||||
// false), so the box renders unchecked before the first read lands.
|
||||
revokeddeniessharedkey: false,
|
||||
maxpendingenrollments: DEFAULT_PENDING_CAP
|
||||
})
|
||||
// Bound with v-model rather than :value so the field holds what the operator
|
||||
@@ -697,6 +721,8 @@ async function loadEnrollment({ quiet = false } = {}) {
|
||||
enrollmentSettings.value = {
|
||||
sharedkeyallowed: data.settings?.sharedkeyallowed !== false,
|
||||
autoenroll: data.settings?.autoenroll !== false,
|
||||
// Opt-in, so anything but an explicit true reads as off.
|
||||
revokeddeniessharedkey: data.settings?.revokeddeniessharedkey === true,
|
||||
maxpendingenrollments: Number.isFinite(Number(data.settings?.maxpendingenrollments))
|
||||
? Number(data.settings.maxpendingenrollments)
|
||||
: DEFAULT_PENDING_CAP
|
||||
@@ -771,6 +797,26 @@ function onAutoEnrollToggle(event) {
|
||||
})
|
||||
}
|
||||
|
||||
function onRevokedDeniesToggle(event) {
|
||||
const turningOn = event.target.checked
|
||||
if (turningOn) {
|
||||
// Typed confirm, like the shared-key switch: this one silences PCs that
|
||||
// are reporting right now, and the revoke-plus-rollback recipe in the
|
||||
// runbook stops working the moment it is on.
|
||||
const answer = prompt(
|
||||
'Deny the shared key to revoked PCs?\n\n'
|
||||
+ 'Any PC whose credential was revoked stops reporting and stops '
|
||||
+ 'receiving commands until it enrolls again. The revoke + block '
|
||||
+ 're-enrollment rollback onto an older agent no longer works.\n\n'
|
||||
+ 'Type DENY to confirm.')
|
||||
if (!answer || answer.trim().toUpperCase() !== 'DENY') {
|
||||
event.target.checked = false
|
||||
return
|
||||
}
|
||||
}
|
||||
return saveSettings({ revokeddeniessharedkey: turningOn })
|
||||
}
|
||||
|
||||
function onPendingCapChange() {
|
||||
const wanted = Math.trunc(Number(pendingCapInput.value))
|
||||
if (!Number.isFinite(wanted) || wanted < 0) {
|
||||
|
||||
Reference in New Issue
Block a user