Skip to content

cert_options.console

Configures issuance of console certificates: the certificate that authenticates an interactive console login on a machine with no browser in front of it, where the approval travels as a short code the human reads off the screen and types into the web UI (see https://mnestor.github.io/ssoossh/concepts/console-flow/).

Deliberately its own type rather than a flag on cert_options.pam. A console certificate buys a whole interactive session where a PAM one buys a single local operation, so an operator needs to gate, time, and label the two separately — a sudo may be approvable by a colleague when a console login is not, and the two must stay distinguishable in an audit log.

Key Type Default
cert_options.console.require.group string empty
cert_options.console.require.claim string empty
cert_options.console.require.at_least number 0
cert_options.console.require.at_most number 0
cert_options.console.require.exactly number 0
cert_options.console.require.equals string empty
cert_options.console.require.one_of list empty
cert_options.console.require.contains string empty
cert_options.console.require.all_of list empty
cert_options.console.require.any_of list empty
cert_options.console.allowed_networks list empty
cert_options.console.client_timeout duration 2m
cert_options.console.valid_duration duration 30s
cert_options.console.extensions list empty
cert_options.console.key_id_template string empty
cert_options.console.lifetime_policy.default_duration duration 0
cert_options.console.lifetime_policy.on_absent_claim string empty
cert_options.console.lifetime_policy.default_extensions list empty
cert_options.console.lifetime_policy.default_enrollment_duration duration 0
cert_options.console.lifetime_policy.tiers list empty
cert_options.console.lifetime_policy.tiers[].name string empty
cert_options.console.lifetime_policy.tiers[].when.group string empty
cert_options.console.lifetime_policy.tiers[].when.claim string empty
cert_options.console.lifetime_policy.tiers[].when.at_least number 0
cert_options.console.lifetime_policy.tiers[].when.at_most number 0
cert_options.console.lifetime_policy.tiers[].when.exactly number 0
cert_options.console.lifetime_policy.tiers[].when.equals string empty
cert_options.console.lifetime_policy.tiers[].when.one_of list empty
cert_options.console.lifetime_policy.tiers[].when.contains string empty
cert_options.console.lifetime_policy.tiers[].when.all_of list empty
cert_options.console.lifetime_policy.tiers[].when.any_of list empty
cert_options.console.lifetime_policy.tiers[].max_duration duration 0
cert_options.console.lifetime_policy.tiers[].grant_extensions list empty
cert_options.console.lifetime_policy.tiers[].max_enrollment_duration duration 0
cert_options.console.lifetime_policy.source_policy list empty
cert_options.console.lifetime_policy.source_policy[].cidr string empty
cert_options.console.lifetime_policy.source_policy[].max_duration duration 0
cert_options.console.lifetime_policy.source_policy[].extensions list empty
cert_options.console.lifetime_policy.source_policy[].removed_extensions list empty
cert_options.console.lifetime_policy.source_policy[].pin_source_address bool false

The condition an approver’s identity must satisfy to approve a console login at all. Unset means any authenticated user may.

It is not the per-host gate. Restricting which accounts may console into which machine belongs in the host’s own PAM stack (pam_succeed_if above the ssoossh line), where it is root-owned, unforgeable, and fails before any network call — a group a module sent would be untrusted input that stops applying the moment somebody omits it.

- group: membership in an OIDC group, exactly the behaviour group tiers
had before conditions existed.
- claim with at_least / at_most: numeric comparison against an extra
claim (see authentication.fields.extra). Both keys together express a
range; boundaries are inclusive.
- claim with exactly: numeric equality, shorthand for at_least and
at_most set to the same value.
- claim with equals / one_of: scalar string equality, or membership of
the scalar in a fixed set.
- claim with contains: membership of a value in a list-valued claim.
- all_of / any_of: conjunction or disjunction over a list of the forms
above. One level of nesting only — a nested condition may not itself
carry all_of or any_of.

Exactly one of group, claim, all_of, or any_of must be set. A claim condition takes exactly one comparator family. Every claim name referenced must be declared under authentication.fields.extra, checked at startup.

An absent claim is never neutral: a missing or unparseable claim value fails the condition (the floor), loudly, and can never widen what an identity receives. A claim’s value is only as fresh as the subject’s last login — see authentication.fields.extra.

string, default empty

Names an OIDC group the identity must be a member of.

string, default empty

Names an extra claim (a key under authentication.fields.extra) the comparator keys below test. The server attaches no meaning to the claim itself; it only compares the value.

number, default 0

Passes when the claim’s numeric value is >= this bound (inclusive). May be combined with at_most to express a range.

number, default 0

Passes when the claim’s numeric value is <= this bound (inclusive). May be combined with at_least to express a range.

number, default 0

Passes when the claim’s numeric value equals this value. It desugars to at_least and at_most of the same value, so there is no second comparison path. Right for an integer-valued score; a computed confidence of 39.9999 does not equal 40, so a non-integral literal here draws a startup warning.

string, default empty

Passes when the claim’s scalar string value equals this string.

list, default empty

Passes when the claim’s scalar string value appears in this set.

string, default empty

Passes when this value appears in a list-valued claim. A scalar claim takes the absent path — the condition fails, loudly.

list, default empty

Passes when every listed condition passes. Listed conditions may not themselves carry all_of or any_of; nesting stops at one level.

list, default empty

Passes when at least one listed condition passes. Listed conditions may not themselves carry all_of or any_of; nesting stops at one level.

list, default empty

Refuses request creation from outside these CIDRs, before a keypair is certified and before any human is asked to approve anything. Empty means no network gate.

The server’s half of per-host policy rests on the source address rather than the hostname because the address is observed by the server and the hostname is a string an unauthenticated caller typed — the same reasoning that got host certificates declined (https://mnestor.github.io/ssoossh/project/decisions/). Behind a reverse proxy this is only meaningful with http.trusted_proxies set; without it every request carries the proxy’s address.

Every refusal writes one cert.request_refused_network audit event with the observed source address (https://mnestor.github.io/ssoossh/operations/audit-log/). The caller needs no session to cause one, so keep http.rate_limit on when the server is reachable from the internet.

cert_options:
console:
allowed_networks: []

duration, default 2m

This type’s whole budget: the longest a console login can sit waiting for a human. Unset (zero) inherits cert_options.client_timeout. A value LONGER than that global is a startup error — the global is the ceiling, and a type may only shorten it.

Shorter is the point. The approval window is the attacker’s working time in the consent-phishing case the typed code exists to raise the bar on: the attacker is the one at the unattended console, and has to phone a victim, read them the code off its screen, and talk them through approving it. Halving the window halves the time that call has to succeed in.

The human’s share is client_timeout - 2*(client_timeout/10), so 2m here gives the approver 96s, not 120s. There is a floor, and it is not the technical one: below about 90s a first approval that has to go through an OIDC login starts to fail, people retry, and a flow people habitually retry is a flow people learn to approve without reading.

cert_options:
console:
client_timeout: 2m

duration, default 30s

The ceiling on how long an issued console certificate is valid. Seconds, not hours, for the same reason as the PAM type: it is validated once by the module and discarded, and the session it authorizes outlives it by design.

cert_options:
console:
valid_duration: 30s

list, default empty

Default to empty. permit-pty and friends are meaningless for a certificate that authenticates one local login and is then thrown away.

cert_options:
console:
extensions: []

string, default empty

The key ID written into console certificates. Like the PAM template and unlike the service one, it does NOT fall back to cert_options.user.key_id_template: a console login and an SSH login by the same person must stay distinguishable in an audit log, so an unset template identifies the type instead.

cert_options:
console:
key_id_template: ""

Takes the same grammar as the other types. As with PAM, in practice only the require gate matters: duration tiers against a 30-second certificate and extension grants against an empty ceiling have nothing to do.

duration, default 0

The duration applied when no tier matches. It is required whenever any part of the lifetime policy is configured: a zero value is a startup error rather than a zero-second certificate that fails at signing, several layers from the config line that caused it.

string, default empty

States what a missing or unparseable claim resolves to during condition evaluation. The only accepted value is “floor” (the default): the condition fails and the identity falls through to the floor. It must never mean “skip this condition”, which is how a missing claim becomes the most generous outcome, so no other value exists; the key is here to make the posture explicit in config.

cert_options:
console:
lifetime_policy:
on_absent_claim: ""

list, default empty

The SSH certificate extensions granted when no tier matches, or when the winning tier states no grant_extensions of its own. Applies only when tiers are configured: with tiers present, extension grants are opt-in and start from nothing — an empty or omitted list grants no extensions. Every entry must appear in the enclosing type’s extensions ceiling, checked at startup. With no tiers configured, the grant axis is inactive and the type’s extensions ceiling alone bounds a request.

lifetime_policy.default_enrollment_duration

Section titled “lifetime_policy.default_enrollment_duration”

duration, default 0

The enrollment-code lifetime applied when no tier matches, clamped to cert_options.service.enrollment_duration. Service certificates only — a startup error on any other type. Zero falls back to the enrollment_duration ceiling.

list, default empty

Evaluated in order; the FIRST tier whose when condition the approver’s identity satisfies wins, and the list means what it says — tier order is the administrator’s job. Numeric thresholds are nested by construction (everyone satisfying at_least 40 also satisfies at_least 30), so write them in descending order; ascending order silently lands every high-score identity in the shortest tier. An empty tiers list means DefaultDuration is always used.

string, default empty

Labels the tier for the policy explanation recorded with each approval decision — the answer to “why one hour”.

The condition an identity must satisfy to take this tier. It is required: a tier without one is a startup error. Group tiers from before the condition grammar move from group: <name> to when: {group: <name>}.

- group: membership in an OIDC group, exactly the behaviour group tiers
had before conditions existed.
- claim with at_least / at_most: numeric comparison against an extra
claim (see authentication.fields.extra). Both keys together express a
range; boundaries are inclusive.
- claim with exactly: numeric equality, shorthand for at_least and
at_most set to the same value.
- claim with equals / one_of: scalar string equality, or membership of
the scalar in a fixed set.
- claim with contains: membership of a value in a list-valued claim.
- all_of / any_of: conjunction or disjunction over a list of the forms
above. One level of nesting only — a nested condition may not itself
carry all_of or any_of.

Exactly one of group, claim, all_of, or any_of must be set. A claim condition takes exactly one comparator family. Every claim name referenced must be declared under authentication.fields.extra, checked at startup.

An absent claim is never neutral: a missing or unparseable claim value fails the condition (the floor), loudly, and can never widen what an identity receives. A claim’s value is only as fresh as the subject’s last login — see authentication.fields.extra.

string, default empty

Names an OIDC group the identity must be a member of.

string, default empty

Names an extra claim (a key under authentication.fields.extra) the comparator keys below test. The server attaches no meaning to the claim itself; it only compares the value.

number, default 0

Passes when the claim’s numeric value is >= this bound (inclusive). May be combined with at_most to express a range.

number, default 0

Passes when the claim’s numeric value is <= this bound (inclusive). May be combined with at_least to express a range.

number, default 0

Passes when the claim’s numeric value equals this value. It desugars to at_least and at_most of the same value, so there is no second comparison path. Right for an integer-valued score; a computed confidence of 39.9999 does not equal 40, so a non-integral literal here draws a startup warning.

string, default empty

Passes when the claim’s scalar string value equals this string.

list, default empty

Passes when the claim’s scalar string value appears in this set.

string, default empty

Passes when this value appears in a list-valued claim. A scalar claim takes the absent path — the condition fails, loudly.

list, default empty

Passes when every listed condition passes. Listed conditions may not themselves carry all_of or any_of; nesting stops at one level.

list, default empty

Passes when at least one listed condition passes. Listed conditions may not themselves carry all_of or any_of; nesting stops at one level.

duration, default 0

The longest lifetime certificates in this tier can receive, bounded by the enclosing type’s valid_duration ceiling.

list, default empty

The SSH certificate extensions this tier grants. Every entry must appear in the enclosing type’s extensions ceiling — a grant outside it is a startup error rather than a silent trim. An empty or omitted list falls back to the policy’s default_extensions.

lifetime_policy.tiers[].max_enrollment_duration

Section titled “lifetime_policy.tiers[].max_enrollment_duration”

duration, default 0

Tiers the enrollment code’s own lifetime, clamped to cert_options.service.enrollment_duration — the lever against a code outliving the conditions that authorized it, without re-evaluating anything at retrieve. Service certificates only — a startup error on any other type. Zero falls back to default_enrollment_duration.

list, default empty

Restricts certificate lifetime based on the request’s source IP address. Longest prefix match wins; ties resolve to the stricter rule. Entries are intersected with the tier-determined duration, and the final effective lifetime is clamped to the ceiling set by the enclosing CertOptions*.ValidDuration.

See https://mnestor.github.io/ssoossh/operations/certificate-policy/ section “Which address” for why the server-observed source IP is used, and why RequestedOptions.SourceAddresses is never consulted.

string, default empty

The IPv4 or IPv6 network this rule applies to, in CIDR notation (e.g., “10.0.0.0/8” or “2001:db8::/32”).

lifetime_policy.source_policy[].max_duration

Section titled “lifetime_policy.source_policy[].max_duration”

duration, default 0

The longest lifetime certificates from this network can receive. The final effective duration is min(tier_duration, source_rule_max_duration, type_ceiling).

lifetime_policy.source_policy[].extensions

Section titled “lifetime_policy.source_policy[].extensions”

list, default empty

Has been replaced by removed_extensions. The old key made an empty list and an omitted field mean opposite things at one length check; the subtractive key retires that. Setting it is a startup error rather than a silently skipped narrowing.

lifetime_policy.source_policy[].removed_extensions

Section titled “lifetime_policy.source_policy[].removed_extensions”

list, default empty

SSH certificate extensions requests from this network never receive, subtracted after the tier grant. An empty or omitted list removes nothing — the two spellings agree. Subtractive on purpose: identity grants, network narrows — being on the office range is not a reason to receive a capability the tier withheld.

lifetime_policy.source_policy[].pin_source_address

Section titled “lifetime_policy.source_policy[].pin_source_address”

bool, default false

when true, adds a critical “source-address” SSH option pinning the certificate to this network. Valid only for service certificates; ignored for user certificates (see https://mnestor.github.io/ssoossh/operations/certificate-policy/ “Not for user certificates”). The network must be narrow enough to actually restrict — a /0 or ::/0 with PinSourceAddress=true is a warning sign (the certificate can be used anywhere, pinning is meaningless). Narrowing is enforced by the intersectExtensions helper (source-address is not an extension).