Make rollback, first upgrades and pin changes work as tested on Windows
Found in an end-to-end test on a Windows 11 VM against a self-signed server.
- Rollback: a 1.20 agent re-enrolls within a second of an un-enroll, so an
older agent pushed afterwards arrived on an enrolled PC and was refused.
Revoke now takes {"blockreenroll": true, "reason": ...}; a blocked PC is
never issued a credential and keeps working on the shared key until
POST .../assets/<assetid>/allow-reenroll. The Enrollment page offers the
block when un-enrolling and shows blocked PCs with an allow button.
- First upgrade of old agents: agents before 1.20 check the server
certificate when downloading the upgrade, so a store wave failed on PCs that
do not trust a self-signed server while the Migration tab said ready. A
failed download now marks the PC blocked with "server certificate not
trusted by this PC". New Install-ShopDBServerCertificate.ps1 (GE-Enforce,
SYSTEM) adds a non-CA server certificate to the trusted roots after checking
its thumbprint; the server certificate page offers a .cer download and the
SHA-1 thumbprint.
- The generated installer writes ServerPins (with an optional next pin for a
planned rotation), can leave the shared key out so the PC waits for
approval, never touches an existing credential, and skips the trusted-root
import for a CA certificate.
- The agent re-reads server pins about every 60 seconds and recycles pooled
connections within 2 minutes, so a pin change needs no restart.
- An alert can only be acknowledged by a PC it targets.
- A wave no longer counts a PC as failed after a later wave upgraded it.
- The scripts report a pin mismatch only when the pin really is missing, and
otherwise log the real error.
- Runbook: trust step before the first wave, rollback order, pin re-read,
verifying the pin against the live URL; agent contract updated.
This commit is contained in:
@@ -374,29 +374,83 @@
|
||||
<tr v-if="!credentialRows.length">
|
||||
<td :colspan="isadmin ? 8 : 7" class="empty-cell">No computers match.</td>
|
||||
</tr>
|
||||
<tr v-for="row in credentialRows" :key="row.assetid">
|
||||
<td>{{ row.hostname || '-' }}</td>
|
||||
<td>{{ row.serialnumber || '-' }}</td>
|
||||
<td><span class="state-chip" :class="row.state">{{ stateLabel(row.state) }}</span></td>
|
||||
<td>
|
||||
<code v-if="row.credentialprefix" class="mono">{{ row.credentialprefix }}...</code>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>{{ formatDate(row.issuedat) }}</td>
|
||||
<td>{{ formatDate(row.lastusedat) }}</td>
|
||||
<td>{{ row.enrolledvia || '-' }}</td>
|
||||
<td v-if="isadmin" class="action-cell">
|
||||
<button
|
||||
v-if="row.state === 'enrolled'"
|
||||
type="button"
|
||||
class="btn btn-danger btn-sm revoke"
|
||||
:disabled="busy"
|
||||
@click="revoke(row)"
|
||||
>
|
||||
Un-enroll
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="row in credentialRows" :key="row.assetid">
|
||||
<tr class="credential-row">
|
||||
<td>{{ row.hostname || '-' }}</td>
|
||||
<td>{{ row.serialnumber || '-' }}</td>
|
||||
<td>
|
||||
<span class="state-chip" :class="row.state">{{ stateLabel(row.state) }}</span>
|
||||
<span
|
||||
v-if="row.reenrollblocked"
|
||||
class="state-chip blocked block-chip"
|
||||
:title="row.reenrollblockedreason || ''"
|
||||
>Re-enroll blocked</span>
|
||||
</td>
|
||||
<td>
|
||||
<code v-if="row.credentialprefix" class="mono">{{ row.credentialprefix }}...</code>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>{{ formatDate(row.issuedat) }}</td>
|
||||
<td>{{ formatDate(row.lastusedat) }}</td>
|
||||
<td>{{ row.enrolledvia || '-' }}</td>
|
||||
<td v-if="isadmin" class="action-cell">
|
||||
<button
|
||||
v-if="row.state === 'enrolled'"
|
||||
type="button"
|
||||
class="btn btn-danger btn-sm revoke"
|
||||
:disabled="busy"
|
||||
@click="openRevoke(row)"
|
||||
>
|
||||
Un-enroll
|
||||
</button>
|
||||
<button
|
||||
v-if="row.reenrollblocked"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm allow-reenroll"
|
||||
:disabled="busy"
|
||||
@click="allowReenroll(row)"
|
||||
>
|
||||
Allow re-enrollment
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="revoking && revoking.assetid === row.assetid" class="revoke-row">
|
||||
<td :colspan="isadmin ? 8 : 7">
|
||||
<p class="field-hint">
|
||||
Un-enroll {{ revoking.hostname }}? Its credential stops working
|
||||
immediately. {{ revokeFallback }}
|
||||
</p>
|
||||
<label class="checkbox-row">
|
||||
<input v-model="revoking.block" type="checkbox" class="revoke-block" />
|
||||
<span>
|
||||
Keep this PC from enrolling again (use before rolling back to
|
||||
an older agent, or for a compromised PC)
|
||||
</span>
|
||||
</label>
|
||||
<div class="approve-form">
|
||||
<input
|
||||
v-if="revoking.block"
|
||||
v-model="revoking.reason"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
class="form-control revoke-reason"
|
||||
placeholder="Reason (optional)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger btn-sm revoke-confirm"
|
||||
:disabled="busy"
|
||||
@click="confirmRevoke"
|
||||
>
|
||||
Un-enroll
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="revoking = null">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -518,7 +572,9 @@ function waveConfirmText(count) {
|
||||
return `Upgrade ${count} PC(s) to endpoint agent ${target.value}?\n\n`
|
||||
+ 'Each PC downloads the new agent and restarts its service. Once the new '
|
||||
+ 'agent is running it enrolls automatically with the shared key it already '
|
||||
+ 'holds; no further action is needed unless it lands in Pending.'
|
||||
+ 'holds; no further action is needed unless it lands in Pending.\n\n'
|
||||
+ 'PCs on agents older than 1.20 must trust the server certificate before '
|
||||
+ 'they can download the upgrade (see runbook).'
|
||||
}
|
||||
|
||||
async function startWave(body, count) {
|
||||
@@ -729,15 +785,37 @@ async function reject(request) {
|
||||
}
|
||||
}
|
||||
|
||||
async function revoke(row) {
|
||||
const fallback = enrollmentSettings.value.sharedkeyallowed
|
||||
const revoking = ref(null)
|
||||
|
||||
const revokeFallback = computed(() => {
|
||||
if (revoking.value?.block) {
|
||||
return 'It stays un-enrolled and keeps reporting on the shared key while that is allowed.'
|
||||
}
|
||||
return enrollmentSettings.value.sharedkeyallowed
|
||||
? 'It falls back to the shared key and enrolls again on its own.'
|
||||
: 'The shared key is off, so it cannot report until an admin approves its new request.'
|
||||
if (!confirm(`Un-enroll ${row.hostname}?\n\nIts credential stops working immediately. ${fallback}`)) return
|
||||
})
|
||||
|
||||
function openRevoke(row) {
|
||||
revoking.value = { assetid: row.assetid, hostname: row.hostname, block: false, reason: '' }
|
||||
}
|
||||
|
||||
async function confirmRevoke() {
|
||||
const current = revoking.value
|
||||
if (!current) return
|
||||
busy.value = true
|
||||
try {
|
||||
await api.post(`/endpoints/enrollment/assets/${row.assetid}/revoke`)
|
||||
toast.success(`Un-enrolled ${row.hostname}`)
|
||||
const url = `/endpoints/enrollment/assets/${current.assetid}/revoke`
|
||||
if (current.block) {
|
||||
const body = { blockreenroll: true }
|
||||
const reason = (current.reason || '').trim()
|
||||
if (reason) body.reason = reason
|
||||
await api.post(url, body)
|
||||
} else {
|
||||
await api.post(url)
|
||||
}
|
||||
toast.success(`Un-enrolled ${current.hostname}`)
|
||||
revoking.value = null
|
||||
await loadEnrollment({ quiet: true })
|
||||
} catch (error) {
|
||||
toast.error(apiError(error, 'Could not un-enroll'))
|
||||
@@ -746,6 +824,21 @@ async function revoke(row) {
|
||||
}
|
||||
}
|
||||
|
||||
async function allowReenroll(row) {
|
||||
if (!confirm(`Allow ${row.hostname} to enroll again?\n\n`
|
||||
+ 'Its agent enrolls on its next attempt if it runs agent 1.20 or later.')) return
|
||||
busy.value = true
|
||||
try {
|
||||
await api.post(`/endpoints/enrollment/assets/${row.assetid}/allow-reenroll`)
|
||||
toast.success(`${row.hostname} may enroll again`)
|
||||
await loadEnrollment({ quiet: true })
|
||||
} catch (error) {
|
||||
toast.error(apiError(error, 'Could not allow re-enrollment'))
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Shared -----------------------------------------------------------
|
||||
|
||||
function stateLabel(state) {
|
||||
@@ -982,6 +1075,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
.approve-form .form-control { max-width: 260px; }
|
||||
|
||||
.block-chip { margin-left: 0.35rem; }
|
||||
|
||||
.revoke-row .form-control { max-width: 260px; }
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
|
||||
Reference in New Issue
Block a user