Repository navigation
Add security keys, the certificate names they need, and finish the console kit - #47
Merged
Merged
Conversation
…nsole kit Derive the WebAuthn relying party from the request so there is nothing to configure, say why a key cannot be used where it cannot, add TLS_SAN and report a certificate that predates the configured names, move the last eight pages onto the shared kit with real loading and failure states, correct the agent module reference, and fix the event parser that dropped every PowerShell 403's first field.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security keys, the certificate names they need, and the console around them
Three pieces that turned out to be one: a security key cannot be registered
against an IP address, this deployment's certificate only ever answered to
localhost, and the pages that would host the feature were the ones stilloutside the shared UI kit.
1. Certificate names
The shipped certificate carried this:
The SAN list was
[cn, "localhost"]and nothing else — hence the duplicate,and hence no way to add a name at all. A console opened at
https://192.168.1.26:8000failed hostname verification in every browser, ontop of the untrusted-CA warning: two errors that look like one.
TLS_SANis comma separated and takes hostnames and addresses alike. An entrythat parses as an address becomes an IP SAN, because a bare address in the
DNS list matches nothing.
localhost,127.0.0.1and::1are alwaysincluded, and names are deduplicated:
The part that was actually costing time.
ensure_certsis idempotent —correctly, since a new identity on every restart breaks every agent that pinned
the CA. The consequence is that changing
TLS_CNon a deployment that alreadyhas a certificate does nothing, and nothing said so. The operator sets the
hostname they intend to use, restarts, gets the same
localhostcertificate,and spends the afternoon on the browser warning. It now says so at startup,
with the command to fix it, verified against the live certificate:
That check silently vanished on its first draft —
except: returnmade "couldnot read the certificate" and "the names match" produce identical output, which
is the failure the function exists to report. It has its own test now.
2. Security keys
A one-time code typed into a convincing copy of this login page works on the
real one, and the operator has no way to tell. A key signs over the origin it
is actually talking to, so it produces nothing usable on a copy.
There is no configuration, and that was the design question. The
relying-party ID is derived from the request. A laptop at
https://localhost:8000and an estate athttps://soc.example.comboth workwithout being told anything, and no setting can disagree with the URL people
type.
The cost is that some origins cannot host a key, and the console says which
rather than showing a button that fails. Verified in the running container:
Plain HTTP, and a server installed without the
webauthnpackage, are reportedthe same way. One-time codes are unaffected in every case — the message
says so, because an operator who reads "unavailable" as "two-factor is off
here" turns the whole thing off.
Three things worth reading in review:
localhostis onethe browser will not offer at a hostname. Without the column the console
lists a factor that cannot work and the login prompt times out silently.
_has_security_keyis deliberately not scoped to the RP ID. Whether alogin needs a second factor is a property of the account; whether this
browser can offer a key is a property of the origin. Conflating them would
let an operator with a key registered at a hostname skip the second factor
entirely by opening the console at an address — turning the origin binding,
the entire point of WebAuthn, into a bypass.
the last one seen is refused and raises
WEBAUTHN_COUNTER_REGRESSED(CRITICAL). Zero means "not implemented" — true of most platform passkeys and
everything that syncs — and is accepted, because refusing it would switch the
control off for the hardware people actually have.
The library is imported lazily, so a deployment that pulls new code without
reinstalling loses the key option rather than the console.
Existing invariants caught three things on the way in: the new routes were not
declared in the auth-wiring list,
WEBAUTHN_COUNTER_REGRESSEDwas raisedbefore it was defined, and
webauthnwas in requirements.txt but not the lock.The lock was regenerated inside the image;
cryptographymoved 43 → 50 and thewhole suite was run against it before the pin was added.
base64url is not base64
frontend/src/lib/webauthn.tshas its own test file because this is whereWebAuthn fails intermittently.
-and_stand in for+and/, padding isdropped, and
atobaccepts neither substitution. The two alphabets agree untila byte lands on one of the four differing characters — so an implementation
that forgets to translate passes a demo, passes review, and then throws
InvalidCharacterErrorfor one operator in three, naming nothing.3. The console (C2)
All eight remaining pages moved onto the shared kit: 460 inline style objects
→ 171, and eight hand-rolled modal overlays → the kit's
Modal.The styling was the smaller half.
console.errorand anempty list, or no
catchat all. The consequences were not equal. An emptyIdentity & Access reads as "nobody has access". Blank AdminConfig forms read
as "nothing is configured" and those forms save, so a working SMTP or
LDAP configuration could be overwritten with a guess. A freshly issued
enrolment token showed as "No enrollment tokens yet", whose natural response
is to issue another.
runs steps its own editor labels "cannot be undone from the console". A run
that never reached the server and one that failed on the host looked
identical — and the natural response to the first, press it again, is the
worst response to the second.
rendered unconditionally with no request behind it, green while the worker
was down. Removed.
will not be shown again";
navigator.clipboardonly exists in a securecontext, so over plain http the copy did nothing and the button still said
"Copied".
Badge.zIndex100/1000/9999 between them. The kit'sModalcloses on Escape andon the backdrop — neither was true anywhere before, and the delete
confirmations had no Cancel button. It portals to
document.body, becauseposition: fixedresolves against the nearest containing block and a dialogwritten inside a card comes out half off-screen; AgentDetail hit exactly that
and carries a comment about it.
input, select, textarearule inindex.css with their own, at a different radius — two of them re-implementing
:focusby hand inonFocus/onBlur.4.
Sentora/docs/MODULES.mdNothing checked this file, and it had drifted badly.
installed_software,resource_log,disk_info. The real ones aresoftware_inventory,resource_usage,disk_usage.edr_enforcer's entry was wrong end to end. Documented as a hash-baselinescan writing
security_audit; it is the agent's main periodic collector andwrites
fim_data,network_connections,process_events,hardware_inventoryandregistry_logs. Four of those appeared nowhere.security_audit, in full. Three modules were documented as writing it.None ever has — nothing in the agent has. The document promised it, the
schema created it,
TABLESshipped it empty every cycle, and the consoleshowed NOT COLLECTED for the life of every install: a sensor that reads as
broken rather than as never built. The other two actually write
events_alertthrough
send_alert().tests/test_module_reference.pynow holds the line in both directions: everydocumented output table must exist in the schema, and every table the agent
ships must be named somewhere.
While correcting the paragraph about
scrub_nuls, a real NUL byte got intothe file from an escape that collapsed one layer too far — enough to make
greptreat it as binary and go quiet. Fixed, with a test.5.
windowsEvent.ts, found by runningnpm testThe frontend suite was already red.
parseEventMessage's field pattern is anchored at the start of a line, but theagent builds its message as
summary | insertson one line, so the first fieldarrives after the headline separated by a tab:
Every event of that shape lost its first field — the one that says what
happened — from the detail view. The test for it existed and was failing.
The same function trimmed with
(\s*\|\s*)+$: a quantifier inside a quantifierwith whitespace ambiguous between adjacent repetitions, which backtracks
exponentially on a line ending in a long run of spaces. This runs in the
operator's browser over text an attacker writes into a log.
[\s|]+$is stillsuper-linear — an anchored trailing match retries from every position on
failure, and JavaScript has no possessive quantifiers — so it is a one-pass
scan now.
Testing
2601 passed, 5 skipped(Python) ·42 pass, 0 fail(frontend) · typecheck andbuild clean.
Verified against the running deployment, not only in tests:
portscan_resulttarget_ip/statesoftware_inventoryon the servernetwork_inventoryon the serverA string literal cannot contain NUL/api/2fa/webauthn/capabilityanswers 401 unauthenticated rather than 500, theagent is connected over
wss://, and there are no tracebacks at startup.