Decision log¶
port49 writes one JSON record for every decision it makes, and each record is one line of the log. The log is port49's own record of what it decided, for whom and against which rule. It is separate from the accounting records a device sends, which report what a device did. Every key is snake_case.
Every record has the same envelope: ts, event and, for a decision, verdict and reason. The blocks
that follow (auth, device, session and the rest) appear on the records they apply to. Most fields are
left out of a record when they have no value, so a query must treat a missing key as "not set".
A record that names its policy blob can be replayed offline and must give the same verdict. The
reason codes page lists every value of reason.
The meanings are the policy-engine spec's own text, so a section or decision number in one points into the spec. A lab ID in a meaning is an entry of the spec's lab register, marked open or closed. It stays open until a lab test has measured the behavior it names, and a claim that cites an open entry is not yet measured.
Events¶
A decision event records one decision, with a verdict and a reason.
| Event | Emitted by | What it decides |
|---|---|---|
mint |
broker | which grants matched, which roles composed, which blob resulted. A record a sweep page writes through the mint (POST /v1/tokens by scope) carries sweep: {scope, after}; the revocation re-check's records after the mint do not (§13 decision 42 ruling 4). A record of a device under a per-login requirement carries per_login_mfa: {accept, reuse_for_seconds, levels, method, proved_at, reused} (§5.1.5) |
redeem |
daemon | whether a presented credential is valid, and creates the context |
authorize_session |
daemon | the session and its returned attributes (§6) |
authorize_command |
daemon | one command vector |
escalate |
daemon | an enable exchange, attributed to the principal (§7) |
refresh |
broker | whether a live context takes the policy now in force, or is revoked (§5.1.3); or, on a session refresh (/v1/refresh), whether it is extended, with session.auth_time and session.session_expires (§5.6); or, on a policy refresh, session.session_expires where the ceiling moved earlier. A context on a platform that never asks about a command carries session.unreached: true (decision 21). On a session refresh, a context under a requirement no proof covers is deny per_login_mfa_required, not extended (§5.1.5). A refresh record under a requirement carries per_login_mfa: {…} as the mint's does |
revoke |
broker | a revocation by principal or by certificate (§5.1.4): what it reached, and the live contexts it could not; a revocation lifted; or, over HTTP, a principal ending its own sessions (by: self) or a (device, username) key ended (by: key), neither leaving a mark |
identity |
broker | what GET /v1/identity resolved the caller to: permit grants_matched when its subject holds a grant unexpired now, else deny no_grants_matched (design document §17) |
explain |
broker | a hypothetical decision POST /v1/explain answered, about the caller or, under explain_others, another principal: its verdict and reason are the answer given, or deny api_right_missing. It authorizes nothing, and replay passes over it |
issue |
broker | whether a person's sign-in is exchanged for a port49-issued certificate: an OIDC sign-in at POST /v1/certificate (§13 decision 43 ruling 8), or a directory-password sign-in at POST /v1/sign-in or /v1/sign-in/confirm (§13 decision 47): permit certificate_issued, carrying certificate: {serial, issuer, fingerprint, not_after} and auth.oidc or auth.directory_password: {amr, auth_time, entry_sha256}, or a deny with the refusal's reason. A directory sign-in's record carries sign_in: {source}, and on a deny failed (name, second_factor, second_factor_skew, password, directory_state, enrollment, principal_limit, source_limit) with skew_seconds, directory_result and directory_data where they apply. It never carries the name typed, the password, the code or the secret. A broker that cannot issue (issuance_unavailable) records nothing |
per_login_mfa |
broker | whether a per-login proof posted to POST /v1/mfa is accepted (§5.1.5; §13 decision 45): permit per_login_mfa_proved or deny per_login_mfa_refused, carrying per_login_mfa: {method, challenge_sha256}. A proof the broker cannot judge (store_unavailable) records that |
admin_touch |
broker | an admin-level call's proof outcome (§5.1.6; §13 decision 54 ruling 15): deny admin_touch_required (a challenge was issued, nothing done), permit admin_touch_proved (the operation's own record follows), deny admin_touch_refused or deny admin_touch_unavailable, carrying admin: {operation, target, challenge_sha256, why}, where why is on a refusal or an unavailable answer one of a closed set of fixed words and never text from the request: no_roots, bearer, no_client_certificate, no_exporter, malformed_proof, unknown_challenge, touch_slot_chain, session_slot_chain, device_mismatch, touch_policy, session_key, bad_signature. A record never carries the challenge, the signature, the attestations or the exporter value; the cause text is in the API answer alone |
enroll |
broker | whether a person's TOTP second factor is enrolled (POST /v1/sign-in/confirm; §13 decision 47 ruling 4): permit second_factor_enrolled, carrying auth.directory_password and sign_in: {source}, or a deny with the sign-in's reasons. Every enrollment is alertable: inside an enrollment window a stolen password enrolls the thief's authenticator |
blob |
broker | not a decision: the full compiled blob, emitted once per hash so replay depends on the log alone (§5.1.1) |
Report events¶
A report event shares the envelope but is not a decision: it carries no verdict.
| Event | Emitted by | What it reports |
|---|---|---|
account |
daemon | A device's accounting record: what the device did, after the fact. It carries the record block and no policy block. |
account_gap |
daemon | Accounting records port49 received and did not write to the log, with the gap block: the stage, how many and since when. A window with a gap cannot say a device did not ask. |
unvalidated |
daemon | A packet no key validated, or a connection the listener refused or ended: the unvalidated block, with a diagnosis, the cleartext header fields and a hash of the body. Never the body. |
inventory_change |
daemon and broker | One security-relevant change a policy or inventory swap made, in the change block, with the device in the device block. port49_inventory_changes_total{kind} counts them. |
directory_forget |
broker | An operator ran port49 directory forget for a principal, which deleted the principal's directory membership and the entry it was pinned to. The forget block holds the entry. |
second_factor_reset |
broker | An operator reset a person's second factor (port49 directory reset-second-factor), or port49-broker init-enrollments created the enrollment store. The second_factor_reset block holds the cleared enrollment. |
sign_in_unlock |
broker | An operator cleared a principal's failed-attempt count (port49 directory unlock). The sign_in_unlock block holds the count the broker held. |
enrollments_import |
broker | An operator ran port49 directory import, which filled or replaced the enrollment store. The enrollments_import block holds the record count and the export time. |
generic_platform |
daemon | port49d serves an admitted device whose platform has no adapter, with the set's generic adapter. One record per such device at start and at each bundle swap. It carries no verdict, and the broker does not write it. |
Fields¶
| Field | Type | Description |
|---|---|---|
ts |
timestamp | When the record was written, as an RFC 3339 timestamp. |
event |
string | What the record is about. The two events tables list each value: the decision events and what they decide, then the report events. |
verdict |
string | permit or deny. Absent on the report events, which decide nothing, and on an explain that answered with a mint error. |
reason |
string | Why the verdict is what it is: one value of the closed reason vocabulary. |
principal |
string | Who the decision was about. |
auth.method |
string | How the caller authenticated. |
auth.credential_sha256 |
string | The SHA-256 of the credential presented on the wire. The credential itself is never logged. A mint record has no value here and carries mint.token_sha256 for each token it issued. |
auth.fingerprint |
string | The SHA-256 of a client certificate that the trust anchor verified and the broker then refused. It says whose certificate it was. Identity, not a secret. |
auth.directory.source |
string | Where the directory membership this decision used came from: fresh (read from a server for this request) or grace (the stored membership, still inside the grace period). |
auth.directory.verified_at |
string | When the membership was last verified against the directory. |
auth.directory.server |
string | The directory server the membership was read from. |
auth.directory.site |
string | The directory site the membership was read from. |
auth.oidc.issuer |
string | The issuer of the verified OIDC token. |
auth.oidc.sub |
string | The subject of the verified OIDC token. |
auth.oidc.sid |
string | The identity provider's own session id, when the token carries one. |
auth.oidc.amr |
list of string | The token's authentication methods references. Recorded because how someone authenticated cannot be established afterwards. No decision reads it. |
auth.oidc.acr |
string | The token's authentication context class reference. Recorded and read by nothing. |
auth.oidc.auth_time |
string | When the identity provider says the person authenticated. |
auth.k8s.cluster |
string | The configured name of the cluster whose verifier accepted the service account token. |
auth.k8s.issuer |
string | The issuer of the verified service account token. |
auth.k8s.sub |
string | The subject of the verified service account token. |
auth.k8s.serviceaccount_uid |
string | The UID of the service account. |
auth.k8s.pod |
string | The name of the pod the token was issued to. |
auth.k8s.pod_uid |
string | The UID of the pod the token was issued to. |
auth.k8s.node |
string | The node the pod ran on, when the token names one. |
auth.k8s.jti |
string | The token's id, when it carries one. |
auth.directory_password.amr |
list of string | The methods a directory-password sign-in used. |
auth.directory_password.auth_time |
string | When the sign-in authenticated. |
auth.directory_password.entry_sha256 |
string | A SHA-256 of the directory entry that signed in. Never the password or the code. |
device.nas |
string | The device's address as port49 saw it. |
device.name |
string | The device's name in the inventory. |
device.platform |
string | The device's platform in the inventory. |
device.key_ref |
string | The key that validated the exchange. Set on every record of a validated exchange and on no other. A record from the TLS listener has none. |
device.key_version |
integer | The version of that key. port49 keys status reads a rotation's phase-out from it. |
device.transport |
string | tls on a record from the TACACS+ over TLS (RFC 9887) listener. Absent on the RFC 8907 listener's records. |
device.cert_sha256 |
string | On a TLS record, the SHA-256 of the device's verified leaf certificate. |
session.ctx_id |
string | The id of the context (the session state port49 holds for a login). It joins the records of one login. |
session.username |
string | The username the device sent. Left empty when the device sent a token-shaped username, which the log never carries. |
session.username_redacted |
boolean | true when the device sent a token-shaped username and it was left out of session.username. |
session.replaced_ctx_id |
string | On a redeem that replaced an existing context for the same device and username, the id of the displaced context. A second login by the same principal joins the context and sets nothing here. |
session.replaced_unreadable |
boolean | true when the replaced context could not be opened (sealed under a key that has left the two-key window, or failing authentication), so its id cannot be named. |
session.revoked_at |
string | On a redeem refused credential_revoked, when the revocation that refused it was made. |
session.privilege_clamped.from |
integer | The privilege level the context held before a login joined it. |
session.privilege_clamped.to |
integer | The level it was lowered to, which is the newest login's ceiling. Another window's next command above this level is denied privilege_exceeded. |
session.auth_time |
string | On a refresh record that moved it, the session's new authentication time. |
session.session_expires |
string | On a refresh record that moved it, the session's new ceiling; on a redeem refused session_expired, the ceiling that refused it. |
session.unreached |
boolean | true on a refresh record for a context on a platform that never asks about a command, or on a device whose inventory entry gives no platform. The live session keeps what it had. |
session.port |
string | The port (tty) the device reported on this exchange. |
session.rem_addr |
string | The remote address the device reported on this exchange. |
session.privilege |
integer | On authorize_session and authorize_command, the privilege level in the request's header, which is the level the device asked at. On other events, what that event says: a refresh's is the context's level and a redeem's is the login's start level. |
session.held_privilege |
integer | The privilege level port49 held for the context when the request arrived, before any raise this request makes. Present on authorize records whose context was read and is this user's, denials included. Compare it with session.privilege to see what the device asked for against what port49 held. |
request.service |
string | The service attribute of the request. Recorded on every authorize_session and authorize_command record, denials included, so it can be grouped by. It is checked against the platform's session services and nothing else. |
request.raw |
list of string | The request's arguments as the device sent them, after redaction: a word that matches a secret rule is replaced with «redacted» first, and request.redacted says so. |
request.vector |
list of string | The request's command as a list of words. |
request.redacted |
boolean | true when words were replaced with «redacted» before the record was written. A secret is never logged. |
packet_fields.action |
string | The authentication action, as a name. Authentication requests only. |
packet_fields.authen_type |
string | The authentication type, as a name. |
packet_fields.service |
string | The authentication service, as a name. |
packet_fields.authen_method |
string | The authentication method, as a name. Authorization and accounting requests only. |
packet_fields.priv_lvl |
integer | The privilege level in the request header. |
reply.attributes |
list of string | The attributes a permitted authorization returned to the device, as sent, with every placeholder filled. |
flags |
string | On an account record, the accounting flags the device sent, such as stop. |
attribution |
string | On an account record, whether the record was tied to a login port49 knows. resolved: it was. foreign: a record for a session whose start port49 never saw, on a device where it did watch this user's other sessions begin (a local fallback, a console login, or a device-local account with the same name); this is the value to alert on, and port49_accounting_attributions_total{attribution="foreign"} counts it. system: the device acting on its own behalf. unattributed: none of these. |
task_id |
string | The task_id of an accounting record. |
record.raw |
list of string | On an account record, the accounting arguments as the device sent them, after redaction: a word that matches a secret rule is replaced with «redacted» before the record is written, and record.redacted says so. |
record.vector |
list of string | On an account record, the command as a list of words. |
record.redacted |
boolean | true when words were replaced before the accounting record was written, so no secret reaches the disk. |
record.priv_lvl |
integer | The privilege level in the accounting record. |
record.start_time |
integer | The device's start time for the task, when it sent one. |
record.stop_time |
integer | The device's stop time for the task, when it sent one. |
record.elapsed_time |
integer | The device's elapsed time for the task, when it sent one. |
mint.token_sha256 |
list of string | The SHA-256 of each token the mint issued. The token itself never appears in the log. |
mint.ttl_seconds |
integer | How long the minted tokens live, in seconds. |
mint.blob |
string | The hash of the policy blob the mint resulted in. |
policy.bundle |
string | The policy version the blob was compiled from. |
policy.blob |
string | The hash of the compiled blob the decision was made against. With policy.bundle, it is what makes the record replayable. |
policy.subject |
string | The subject that matched the principal. |
policy.subjects |
list of string | Every subject that matched, sorted, when the principal matches more than one. policy.subject is then absent. |
policy.grants.[].scope |
string | On a mint record, the scope of a grant that selected the device. |
policy.grants.[].roles |
list of string | The roles that grant gives. |
policy.grants.[].expires |
string | When the grant stops matching. Empty means never. |
policy.matched_deny.rule |
list of string | The rule's terms as written. |
policy.matched_deny.specificity |
list of integer | The specificity tuple the rule scored against this command. |
policy.matched_deny.role |
string | The role that carries the rule. The rule is the deny that decided a no_permit_dominates_deny, or the most specific deny a permit overrode. |
policy.matched_permits.[].rule |
list of string | The rule's terms as written. |
policy.matched_permits.[].specificity |
list of integer | The specificity tuple the rule scored against this command. |
policy.matched_permits.[].role |
string | The role that carries the rule. The permits that matched the command are listed most specific first, and the first decided a permit. |
policy.stage |
integer | The 1-based pipeline stage that a pipe_stage_denied refused. The stage's words are in request.raw. |
blob.hash |
string | The hash that names the blob. |
blob.body |
object | On a blob record, the compiled blob whole, emitted once per hash, so replay depends on the log alone. |
blob.redacted |
boolean | true when rule text in the body was redacted before it was logged. The body is then not the blob the hash names, and replay reports every record that names it unverifiable. |
unvalidated.source |
string | The source address of a packet no key validated. |
unvalidated.diagnosis |
string | Why the packet or connection is on this record. no_known_key_matches: no key of the device's key_ref validates it. matches_retired: a retired key validates it, and unvalidated.version says which. unencrypted_refused: the packet was not obfuscated. not_tacacs: the header is not TACACS+. unadmitted_source: the source address is not in the inventory. malformed_after_validation: the key validated and the body did not decode. device_quarantined: the device is admitted but quarantined by policy. admission_refused: the device is in the inventory and admission refused it. admission_withdrawn: a reload renamed, re-keyed or re-platformed the device of an open connection. transport_mismatch: the device reached the listener it does not speak. tls_refused: the TLS listener refused or ended the connection, and unvalidated.tls_outcome says why. obfuscated_over_tls: a packet over TLS with the unencrypted flag clear. |
unvalidated.key_ref |
string | The key tried, when there was one. |
unvalidated.version |
integer | The version of the key (the numbered version within the device's key_ref) that the packet matched, or that the connection speaks. During a key rotation, a matches_retired record carries the retired version a device is still using, which is how you find the devices that have not moved to the new key. Absent where no key matched. |
unvalidated.type |
integer | The packet type in the header. |
unvalidated.seq_no |
integer | The sequence number in the header. |
unvalidated.session_id |
integer | The session id in the header. |
unvalidated.length |
integer | The body length in the header. |
unvalidated.body_sha256 |
string | The SHA-256 of the body. The body itself is never logged. |
unvalidated.transport |
string | tls on a record from the TLS listener. |
unvalidated.tls_outcome |
string | On a record with diagnosis: tls_refused, why the handshake was refused. See the values below. |
unvalidated.detail |
string | A bounded, printable note on a record from the TLS listener, or on a transport_mismatch record from either listener (which names the transport the device record says and the listener it reached). It is never a body. |
unvalidated.cert_sha256 |
string | On a record with diagnosis: tls_refused, the SHA-256 of the certificate the device sent, where it sent one. |
server_msg |
string | The message the reply carried to the person at the device. A denial's names the rule that decided and does not echo the command. |
gap.stage |
string | Where accounting records were dropped: offer_refused (answered ERROR, so the device may send the record again), drain_unparseable or drain_set_aside (answered SUCCESS and not in the log). |
gap.records |
integer | How many accounting records were dropped at this stage. |
gap.since |
string | The earliest arrival among the dropped records, as far as the spool knows it. |
change.kind |
string | What changed. Only security-relevant changes are written: device_added (a device added and admitted), admission_changed (a device already in the inventory becoming admitted), address_added (an address added to a device already in the inventory), key_ref_changed, transport_changed and platform_changed (a device's key assignment, transport or platform changed), and scope_joined or scope_left (a device entering or leaving a scope with security_log: true). |
change.scope |
string | The scope involved, when the change names one. |
change.address |
string | The address involved, when the change names one. |
change.from |
string | The earlier value, on key_ref_changed, transport_changed, platform_changed and admission_changed. |
change.to |
string | The later value, on those kinds and on device_added, where it is admitted. |
change.from_bundle |
string | The policy version before the swap that made the change. |
change.to_bundle |
string | The policy version after it. |
forget.entry_id |
string | The id of the directory entry the principal was pinned to. |
forget.dn |
string | The distinguished name of that entry. |
forget.verified_at |
string | When the entry was last verified or refused. |
forget.refused |
boolean | true when the last answer for the entry was a refusal. |
forget.origin |
string | Who asked for the principal to be forgotten. |
sweep.scope |
string | On a mint record made through a sweep page, the scope the sweep walked. |
sweep.after |
string | The page's cursor. |
revocation.by |
string | What the revocation was made by, such as a principal or a certificate. |
revocation.issuer |
string | The issuer of a certificate revoked by serial. |
revocation.serial |
string | The serial of a certificate revoked by serial. |
revocation.oidc_issuer |
string | The OIDC issuer of a session or subject ended by a back-channel logout or a revocation by sid or sub. |
revocation.sid |
string | The OIDC session id that was ended. |
revocation.sub |
string | The OIDC subject that was ended. |
revocation.principals |
list of string | Every principal a logout reached, when its owner record named more than one. The record's principal is then the first. |
revocation.device |
string | The device a revocation was limited to. |
revocation.username |
string | The username a revocation was limited to. |
revocation.at |
string | When the revocation was made. |
revocation.origin |
string | Who made the revocation. |
revocation.contexts |
integer | How many live contexts it reached. Zero is left out. |
revocation.token_keys |
integer | How many token keys it reached. Zero is left out. |
revocation.connections |
integer | How many connections it ended. Zero is left out. |
revocation.unreached.[].device |
string | A live context the revocation could not reach: its device. |
revocation.unreached.[].username |
string | A live context the revocation could not reach: its username. |
revocation.saved |
boolean | Whether broker.revocations_file was written. Present for a standing mark only: absent for a self or key revocation, which writes none, and on a broker that keeps no revocations_file. |
revocation.error |
string | What went wrong, when something did. |
explain.asked_by |
string | The principal that asked the hypothetical question. |
explain.asked_by_fingerprint |
string | The fingerprint of the certificate the asker presented. |
explain.command |
boolean | true when the question was about a command. The command itself is never recorded. |
explain.mint_error |
string | The mint error the broker would have answered instead of a token. The record then has no verdict. |
certificate.serial |
string | The serial of the certificate issued, as port49 revoke --cert-serial takes it. |
certificate.issuer |
string | The distinguished name of the CA that issued it. |
certificate.fingerprint |
string | Its SHA-256 fingerprint. |
certificate.not_after |
string | When it expires. |
per_login_mfa.accept |
list of string | On a mint or refresh record under a per-login requirement, the proof methods the requirement accepts. |
per_login_mfa.reuse_for_seconds |
integer | How long a proof is reused for, in seconds. |
per_login_mfa.levels |
list of string | The privilege levels the requirement covers. |
per_login_mfa.method |
string | The method of the proof that covered the record, or of the proof a per_login_mfa record judged. |
per_login_mfa.proved_at |
string | When the covering proof was made. |
per_login_mfa.reused |
boolean | true when an earlier proof was reused. |
per_login_mfa.challenge_sha256 |
string | On a per_login_mfa record, the SHA-256 of the challenge. The challenge itself is never logged. |
admin.operation |
string | On an admin_touch record, the operation the proof was for. |
admin.target |
string | The operation's target, as the challenge shows it. |
admin.challenge_sha256 |
string | The SHA-256 of the challenge. The challenge, the signature and the attestation are never logged. |
admin.why |
string | For a refusal or an unavailable answer, a fixed cause. |
sign_in.source |
string | The source network a directory-password sign-in came from. |
sign_in.failed |
string | On a refusal, which check failed. |
sign_in.skew_seconds |
integer | For a clock too far off, by how many seconds. |
sign_in.directory_result |
integer | The directory's result code, on a refusal that has one. |
sign_in.directory_data |
string | The directory's diagnostic data, on a refusal that has one. Never the name typed, the password or the code. |
second_factor_reset.enrolled_at |
string | When the enrollment that was cleared was made. |
second_factor_reset.enrolled_from |
string | Where it was made from. |
second_factor_reset.reopen_until |
string | Until when enrollment is open again. Absent on a store creation. |
second_factor_reset.all |
boolean | true for a reset of every enrollment. |
second_factor_reset.created |
boolean | true when the record is the creation of the enrollment store. |
second_factor_reset.origin |
string | Who made the reset. |
sign_in_unlock.failures |
integer | The failed-attempt count the answering broker held. Absent when its session store did not answer the clear: the unlock is then in force but the count is unknown, which is not zero. |
sign_in_unlock.origin |
string | Who cleared the count. |
enrollments_import.records |
integer | How many enrollment records the import wrote. |
enrollments_import.exported_at |
string | When the imported export was made. |
enrollments_import.replaced |
boolean | true when the import replaced an existing store. |
enrollments_import.origin |
string | Who ran the import. |
index_error |
string | What a failed session-index write cost, on the record for the session that lost it. That session is missing from the index, so a later revocation cannot find it. This is different from revocation.unreached, which lists live contexts a revocation found and could not reach. |
The tls_outcome values¶
A record from the RFC 9887 (TACACS+ over TLS) listener carries transport: "tls". An unvalidated record with diagnosis: tls_refused carries the tls_outcome that says why the handshake was refused. The handshake counter port49_tls_handshakes_total uses the same values as its outcome label. Strike says whether the refusal counts against the source address, as a wrong key does.
tls_outcome |
Means | Strike |
|---|---|---|
version_refused |
The ClientHello offered no TLS 1.3. | yes |
no_client_certificate |
The device sent no certificate. | yes |
unknown_ca |
Its chain reaches no client_ca_file anchor. |
yes |
certificate_expired |
A certificate in its chain is expired or not yet valid. | yes |
certificate_invalid |
Any other path error, a serverAuth-only leaf included. | yes |
certificate_revoked |
A certificate in its chain is revoked. | yes |
status_unavailable |
Its status could not be established under on_unavailable: refuse. A decline: the window stands until it lapses. |
no |
identity_mismatch |
The leaf does not name the device its source address is. | yes |
sni_mismatch |
The ClientHello asked for a server name not in server_names. A decline: the window stands until it lapses. |
no |
early_data_refused |
The ClientHello carried early_data (RFC 9887 section 5.1.2). |
yes |
handshake_timeout |
No handshake within handshake_timeout. |
yes |
handshake_failed |
Anything else: a bad signature, a client abort. | yes |
Two more values are never in a record. They are label values of port49_tls_handshakes_total only, and both mean the device was admitted:
ok: The handshake completed and the device's certificate was admitted. Counted after the status check passes.resumed: The handshake resumed an earlier TLS session and the device was admitted.
packet_fields¶
A record for a validated request carries packet_fields: the request's header fields as names, not numbers. These are action (authentication only), authen_type, service, authen_method (authorization and accounting only) and priv_lvl. A value outside the protocol's list is written unknown_<n>, where <n> is the header byte. A record for an unvalidated packet has no packet_fields; it has the unvalidated block, which holds only cleartext header fields.
What the log never holds¶
The log never contains a token, a password, a challenge or a response. A credential appears only as credential_sha256. The body of an unvalidated packet is never logged, only its hash. A token typed as a username leaves session.username empty and sets session.username_redacted to true.