Show per-PC credentials across the admin interface
- Endpoints (shared key) page: the key is presented as what PCs use until they enroll, with enrollment progress, a link to Enrollment, rewritten guidance, and rotate/revoke confirmations that say how many PCs are not enrolled yet. - Settings cards describe the server certificate as what PCs actually receive, on IIS or Docker; Enrollment has its own icon (the icon set lacked the one the Server Certificate card already asked for). - Server Certificate page checks the certificate the site presents at its public address and compares its pin with the certificate file, warning when they differ. The host comes from site settings only; the check is a TLS handshake, never an HTTP request. - Fleet list: Credential column and filter (enrolled, pending, re-enroll blocked, shared key, not reporting), computed in the list query. - PC detail: credential state, prefix and dates for fleet readers, state only for others, with admin Revoke (optionally blocking re-enrollment) and Allow re-enrollment. - Dashboard: pending enrollment requests, and PCs blocked from re-enrolling for more than seven days. - Runbook: where to find each of these in the app.
This commit is contained in:
@@ -5,15 +5,17 @@
|
||||
</div>
|
||||
|
||||
<p class="setting-description">
|
||||
Manage how shop-floor endpoint agents authenticate to this server, build
|
||||
the agent for this site, and set what it reports and how often.
|
||||
Manage the shared key PCs use until they enroll, build the agent for this
|
||||
site, and set what it reports and how often. Each enrolled PC
|
||||
authenticates with its own credential instead; see Settings > Endpoints
|
||||
> Enrollment.
|
||||
</p>
|
||||
|
||||
<!-- ====================================================================
|
||||
Key management
|
||||
Shared key
|
||||
================================================================= -->
|
||||
<section class="settings-section">
|
||||
<h3>Key management</h3>
|
||||
<h3>Shared key (used until PCs enroll)</h3>
|
||||
<p class="key-warning">
|
||||
STORE YOUR KEY SECURELY. IT IS SHOWN ONLY ONCE AND CANNOT BE RECOVERED.
|
||||
</p>
|
||||
@@ -25,12 +27,40 @@
|
||||
paste the raw key into a chat message or a plain file.
|
||||
</p>
|
||||
<p class="field-hint">
|
||||
The endpoint agent reports with a single managed key scoped to endpoint
|
||||
telemetry only. It is separate from every other collector, so rotating
|
||||
or revoking it stops all endpoint-agent traffic immediately without
|
||||
touching the PC or GE-Enforce collectors.
|
||||
Every PC installed with the shared key holds this same key. It lets a
|
||||
PC that has not enrolled yet report and ask for its own credential. An enrolled PC is refused it and
|
||||
uses its credential instead, so rotating or revoking the key only
|
||||
affects PCs that have not enrolled. It is scoped to endpoint telemetry
|
||||
and separate from every other collector.
|
||||
</p>
|
||||
|
||||
<div v-if="enrollment" class="card form-card enrollment-summary">
|
||||
<div class="enrollment-counts">
|
||||
<span class="enrollment-count">
|
||||
<strong>{{ enrollment.enrolled }}</strong> of {{ enrollment.total }} PCs enrolled
|
||||
</span>
|
||||
<span class="enrollment-count">
|
||||
<strong>{{ enrollment.notenrolled }}</strong> still on the shared key
|
||||
</span>
|
||||
<span class="enrollment-count">
|
||||
<strong>{{ enrollment.requests }}</strong>
|
||||
pending enrollment {{ enrollment.requests === 1 ? 'request' : 'requests' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" :style="{ width: enrolledPercent + '%' }"></div>
|
||||
</div>
|
||||
<p class="field-hint">
|
||||
<template v-if="!enrollment.sharedkeyallowed">
|
||||
<strong>The shared key is switched off</strong> in Enrollment, so PCs
|
||||
that have not enrolled already cannot report with it.
|
||||
</template>
|
||||
Approve requests, move PCs over and retire the shared key in
|
||||
<router-link :to="{ name: 'endpoints-enrollment-settings' }">
|
||||
Settings > Endpoints > Enrollment</router-link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card form-card">
|
||||
<div v-if="message" class="settings-success">{{ message }}</div>
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
@@ -49,8 +79,8 @@
|
||||
<template v-else>Not used yet.</template>
|
||||
</p>
|
||||
<p v-else class="field-hint">
|
||||
No endpoint agent key is active. Generate one to let agents report
|
||||
with a managed credential.
|
||||
No shared key is active. PCs that have not enrolled cannot report
|
||||
or auto-enroll until one is generated.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -85,9 +115,10 @@
|
||||
<div v-if="secret" class="card form-card secret-card">
|
||||
<h4>Save this key now</h4>
|
||||
<p class="field-hint">
|
||||
Anyone with this key can report as an endpoint agent for this site.
|
||||
Treat it like a password. If it is lost you must regenerate, which
|
||||
revokes the old key and forces every agent to be reconfigured.
|
||||
Anyone with this key can report as a not-yet-enrolled endpoint agent
|
||||
for this site and ask to enroll. Treat it like a password. If it is
|
||||
lost you must regenerate, which revokes the old key; PCs that have not
|
||||
enrolled then need the new one.
|
||||
</p>
|
||||
<p class="field-hint">
|
||||
<strong>Recommended:</strong> before you leave this page, open the
|
||||
@@ -110,53 +141,57 @@
|
||||
<summary><h4>How it works</h4></summary>
|
||||
<ul class="field-hint info-list">
|
||||
<li>
|
||||
<strong>Generate</strong> mints a fresh key and, on the first
|
||||
generation, turns off the bootstrap <code>.env</code> fallback so
|
||||
the managed key becomes the only accepted credential. The full
|
||||
secret is shown once - store it in the agent config as the
|
||||
<code>X-API-Key</code> header.
|
||||
<strong>Until a PC enrolls</strong> it reports with this shared key.
|
||||
Agents from 1.20 use it once more to ask for their own credential,
|
||||
and from then on send that credential instead.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Regenerate</strong> is a hard cutover: the previous key is
|
||||
revoked in the same moment the new one is minted. Every deployed
|
||||
agent must be reconfigured with the new key before it can report
|
||||
again.
|
||||
<strong>Enrolled PCs ignore it.</strong> The server refuses the shared
|
||||
key for any PC that holds a credential, so regenerating or revoking
|
||||
it does not touch them.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Revoke</strong> deactivates the key and leaves it off. All
|
||||
endpoint-agent reporting stops until you generate a new key. This is
|
||||
the kill switch.
|
||||
<strong>Generate</strong> mints a key and, the first time, turns off
|
||||
the bootstrap <code>.env</code> fallback. The full secret is shown
|
||||
once; it goes into the agent config as the <code>X-API-Key</code>
|
||||
header.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Regenerate</strong> revokes the previous key in the same
|
||||
moment. PCs that have not enrolled must be given the new key before
|
||||
they can report or enroll again.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Revoke</strong> leaves no shared key. PCs that have not
|
||||
enrolled stop reporting and can no longer auto-enroll; enrolled PCs
|
||||
carry on.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details class="collapsible">
|
||||
<summary><h4>When to rotate</h4></summary>
|
||||
<summary><h4>When to rotate or retire</h4></summary>
|
||||
<ul class="field-hint info-list">
|
||||
<li>
|
||||
<strong>An installer or agent left the secure environment.</strong>
|
||||
If a build carrying the key is copied off the shop floor - onto an
|
||||
engineer's laptop, a USB stick, or a machine that leaves the site -
|
||||
treat the key as compromised and regenerate immediately.
|
||||
<strong>At cutover.</strong> Once every PC has enrolled, switch the
|
||||
shared key off in Enrollment, clear it from every place it lives,
|
||||
then regenerate so any copy you missed is dead. The steps are in the
|
||||
per-PC credentials rollout runbook.
|
||||
</li>
|
||||
<li>
|
||||
<strong>A device is lost, stolen, or decommissioned</strong> while
|
||||
still holding the key, and you cannot confirm the key was wiped.
|
||||
<strong>An installer or key left the secure environment.</strong> A
|
||||
build or file carrying the key copied onto a laptop, a USB stick or a
|
||||
machine that leaves the site. Regenerate; only PCs that have not
|
||||
enrolled need the new key.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Staff offboarding.</strong> Someone who could read the key
|
||||
from a config or build pipeline is leaving; rotate as part of the
|
||||
offboarding checklist.
|
||||
from a config or build pipeline is leaving.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Scheduled hygiene.</strong> Rotate on a routine cadence
|
||||
(for example quarterly) so a silently leaked key has a bounded
|
||||
lifetime.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Suspicious traffic.</strong> The Fleet view shows reports
|
||||
from a host you do not recognize - revoke now, investigate, then
|
||||
regenerate and redeploy to known-good endpoints.
|
||||
<strong>Suspicious traffic.</strong> The Fleet view shows reports from
|
||||
a host you do not recognize. Revoke now, investigate, then regenerate
|
||||
for the PCs that still need it.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -532,7 +567,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import api from '@/api'
|
||||
|
||||
@@ -545,6 +580,9 @@ const secret = ref('')
|
||||
const copied = ref(false)
|
||||
const message = ref('')
|
||||
const error = ref('')
|
||||
// Enrollment progress, from the same counts the Enrollment page shows. Null
|
||||
// when the read fails; the key controls work without it.
|
||||
const enrollment = ref(null)
|
||||
|
||||
// Adoption tool: build a one-shot, agentless enrollment download.
|
||||
const adoptbaseurl = ref('')
|
||||
@@ -613,6 +651,40 @@ function filenameFromResponse(response, fallback) {
|
||||
return match ? decodeURIComponent(match[1]) : fallback
|
||||
}
|
||||
|
||||
const enrolledPercent = computed(() => {
|
||||
const total = enrollment.value?.total || 0
|
||||
return total ? Math.round((enrollment.value.enrolled / total) * 100) : 0
|
||||
})
|
||||
|
||||
async function loadEnrollment() {
|
||||
try {
|
||||
const response = await api.get('/endpoints/enrollment', { params: { perpage: 1 } })
|
||||
const data = response.data.data || {}
|
||||
const counts = data.counts || {}
|
||||
const enrolled = counts.enrolled || 0
|
||||
// pending PCs asked but hold no credential yet: still on the shared key.
|
||||
const notenrolled = (counts.notenrolled || 0) + (counts.pending || 0)
|
||||
enrollment.value = {
|
||||
enrolled,
|
||||
notenrolled,
|
||||
total: enrolled + notenrolled,
|
||||
requests: (data.requests || []).filter(
|
||||
(entry) => entry.status === 'pending' && entry.reason !== 'reenroll blocked').length,
|
||||
sharedkeyallowed: data.settings?.sharedkeyallowed !== false
|
||||
}
|
||||
} catch (loadError) {
|
||||
enrollment.value = null
|
||||
console.error(loadError)
|
||||
}
|
||||
}
|
||||
|
||||
// The number of PCs a change to the shared key reaches, for the confirms.
|
||||
function notEnrolledPhrase() {
|
||||
if (!enrollment.value) return 'Every PC that has not enrolled'
|
||||
const count = enrollment.value.notenrolled
|
||||
return `${count} PC${count === 1 ? '' : 's'} not enrolled`
|
||||
}
|
||||
|
||||
function applyStatus(data) {
|
||||
key.value = data.key || null
|
||||
envkeyfallback.value = !!data.envkeyfallback
|
||||
@@ -632,6 +704,7 @@ onMounted(async () => {
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
loadEnrollment()
|
||||
try {
|
||||
const response = await api.get('/endpoints/siteagent')
|
||||
const status = response.data.data || {}
|
||||
@@ -656,8 +729,9 @@ function applyBinaryStatus(binkey, current, versions) {
|
||||
|
||||
async function regenerate() {
|
||||
if (key.value &&
|
||||
!window.confirm('Regenerate the key? The current key stops working '
|
||||
+ 'immediately and every agent must be reconfigured.')) {
|
||||
!window.confirm('Regenerate the shared key? The current key stops working '
|
||||
+ `immediately. ${notEnrolledPhrase()} will need the new key before they `
|
||||
+ 'can report or enroll. Enrolled PCs are not affected.')) {
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
@@ -684,8 +758,9 @@ async function regenerate() {
|
||||
}
|
||||
|
||||
async function revoke() {
|
||||
if (!window.confirm('Revoke the key? All endpoint-agent reporting stops '
|
||||
+ 'until a new key is generated.')) {
|
||||
if (!window.confirm('Revoke the shared key? '
|
||||
+ `${notEnrolledPhrase()} will lose access: they stop reporting and cannot `
|
||||
+ 'enroll until a new key is generated. Enrolled PCs keep working.')) {
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
@@ -959,6 +1034,24 @@ function editConfiguration() {
|
||||
}
|
||||
.secret-value { padding: 0.5rem; background: var(--color-surface-2, #1e1e1e); }
|
||||
.settings-section { margin-bottom: 2rem; }
|
||||
.enrollment-counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.enrollment-count { font-size: 0.9rem; }
|
||||
.progress-track {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-border, #333);
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-success, #4caf50);
|
||||
}
|
||||
.settings-section > h3 { margin-bottom: 0.25rem; }
|
||||
.info-list { margin: 0.25rem 0 0; padding-left: 1.25rem; }
|
||||
.info-list li { margin-bottom: 0.5rem; }
|
||||
|
||||
Reference in New Issue
Block a user