cert_options.service
Configures issuance of service certificates: the OIDC group required to request one, how long they’re valid for, and which SSH certificate extensions to grant.
require_group
Section titled “require_group”string, default empty
Has been replaced by require (see cert_options.user.require_group). Setting it is a startup error.
cert_options: service: require_group: ""require
Section titled “require”The condition an approver’s identity must satisfy in order to approve a service enrollment. Unset means any authenticated user may. Evaluated against the approver — the human vouching — not the service account receiving the certificate.
- 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.
require.group
Section titled “require.group”string, default empty
Names an OIDC group the identity must be a member of.
require.claim
Section titled “require.claim”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.
require.at_least
Section titled “require.at_least”number, default 0
Passes when the claim’s numeric value is >= this bound (inclusive). May be combined with at_most to express a range.
require.at_most
Section titled “require.at_most”number, default 0
Passes when the claim’s numeric value is <= this bound (inclusive). May be combined with at_least to express a range.
require.exactly
Section titled “require.exactly”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.
require.equals
Section titled “require.equals”string, default empty
Passes when the claim’s scalar string value equals this string.
require.one_of
Section titled “require.one_of”list, default empty
Passes when the claim’s scalar string value appears in this set.
require.contains
Section titled “require.contains”string, default empty
Passes when this value appears in a list-valued claim. A scalar claim takes the absent path — the condition fails, loudly.
require.all_of
Section titled “require.all_of”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.
require.any_of
Section titled “require.any_of”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.
allow_user_accounts
Section titled “allow_user_accounts”bool, default false
Lets an approver name one of their own accounts — their username, or an entry from other_accounts — as the service account of a service enrollment, alongside any account the service_accounts claim gives them.
It exists for the deployment with no service-account claim to map: a person who has to run something unattended under their own account (a cron job, a backup, a scheduled fetch) otherwise has no way to get a reusable code, and ends up leaving an interactive credential on the host instead, which is strictly worse.
The certificate is still a service certificate in every respect: one principal, and the options this block grants rather than the ones cert_options.user grants — which with the default empty Extensions means no permit-pty, and so no shell. The approval page says so before the choice is made, and lists the extensions the certificate will carry, because “a certificate for my own account” is otherwise easy to read as “a certificate I can log in with”.
A deployment that puts permit-pty in Extensions has made service certificates interactive for every account, not just personal ones; that is a decision about this block, not about this setting.
Off by default. Turning it on widens what an approver may mint for themselves from “the accounts a claim vouches for” to “any account they hold”, which is a real widening even though every account in it is already theirs.
cert_options: service: allow_user_accounts: falsevalid_duration
Section titled “valid_duration”duration, default 8760h
The ceiling on how long each certificate produced from an enrollment is valid, bounding every redemption of the code rather than the code itself.
cert_options: service: valid_duration: 8760hextensions
Section titled “extensions”list, default empty
The SSH certificate extensions a service certificate may carry: the ceiling, and so also exactly the set an approver is offered at approval time. A request is narrowed to the intersection of what it asked for and this list, and so is an approver’s choice — nothing outside it can be granted by either.
Two things follow that are worth knowing before setting it.
Setting it alone grants nothing. It makes toggles available on the approval page; an approval nobody touches still produces exactly what the enrollment asked for, which for every enrollment made before service enroll --extensions existed is nothing at all.
It is the only bound on a service certificate’s extensions. The client-side forbidden_certificate_extensions policy (https://mnestor.github.io/ssoossh/hosts/client-enforcement/) has never applied to service certificates and still does not — it is subtracted only on the interactive login path — so an extension ticked here reaches a service certificate even on a machine whose administrative policy forbids that extension for its interactive users. Closing that is tracked in docs/proposals/client-policy-assertions.md.
source-address is a critical option, not an extension, and listing it here is a startup error. Pin a service certificate to a network with lifetime_policy.source_policy[].pin_source_address.
cert_options: service: extensions: []enrollment_duration
Section titled “enrollment_duration”duration, default 8760h
How long the enrollment code minted at approval stays redeemable — deliberately independent of ValidDuration, which bounds each certificate the code produces.
The two answer different questions. ValidDuration is “how long may one issued certificate be used”, and wants to be short. service.enrollment_duration is “how long may this service keep asking for a fresh one”, and wants to be long: the code is reusable precisely so an unattended job can run it from cron. Deriving one from the other collapsed that — shortening a service certificate’s lifetime also killed the code within the same span, so a cron job re-enrolled by hand on every run.
Each redemption gets ValidDuration afresh, measured from the redemption, so a long code never yields a long certificate.
cert_options: service: enrollment_duration: 8760hkey_id_template
Section titled “key_id_template”string, default empty
The key ID written into service certificates; see https://mnestor.github.io/ssoossh/concepts/ (key ID templating). Empty falls back to cert_options.user.key_id_template.
cert_options: service: key_id_template: ""lifetime_policy
Section titled “lifetime_policy”Configures tiered certificate duration based on OIDC group membership and source network narrowing — see https://mnestor.github.io/ssoossh/operations/certificate-policy/.
lifetime_policy.default_duration
Section titled “lifetime_policy.default_duration”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.
lifetime_policy.on_absent_claim
Section titled “lifetime_policy.on_absent_claim”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: service: lifetime_policy: on_absent_claim: ""lifetime_policy.default_extensions
Section titled “lifetime_policy.default_extensions”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.
lifetime_policy.tiers
Section titled “lifetime_policy.tiers”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.
lifetime_policy.tiers[].name
Section titled “lifetime_policy.tiers[].name”string, default empty
Labels the tier for the policy explanation recorded with each approval decision — the answer to “why one hour”.
lifetime_policy.tiers[].when
Section titled “lifetime_policy.tiers[].when”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.
lifetime_policy.tiers[].when.group
Section titled “lifetime_policy.tiers[].when.group”string, default empty
Names an OIDC group the identity must be a member of.
lifetime_policy.tiers[].when.claim
Section titled “lifetime_policy.tiers[].when.claim”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.
lifetime_policy.tiers[].when.at_least
Section titled “lifetime_policy.tiers[].when.at_least”number, default 0
Passes when the claim’s numeric value is >= this bound (inclusive). May be combined with at_most to express a range.
lifetime_policy.tiers[].when.at_most
Section titled “lifetime_policy.tiers[].when.at_most”number, default 0
Passes when the claim’s numeric value is <= this bound (inclusive). May be combined with at_least to express a range.
lifetime_policy.tiers[].when.exactly
Section titled “lifetime_policy.tiers[].when.exactly”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.
lifetime_policy.tiers[].when.equals
Section titled “lifetime_policy.tiers[].when.equals”string, default empty
Passes when the claim’s scalar string value equals this string.
lifetime_policy.tiers[].when.one_of
Section titled “lifetime_policy.tiers[].when.one_of”list, default empty
Passes when the claim’s scalar string value appears in this set.
lifetime_policy.tiers[].when.contains
Section titled “lifetime_policy.tiers[].when.contains”string, default empty
Passes when this value appears in a list-valued claim. A scalar claim takes the absent path — the condition fails, loudly.
lifetime_policy.tiers[].when.all_of
Section titled “lifetime_policy.tiers[].when.all_of”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.
lifetime_policy.tiers[].when.any_of
Section titled “lifetime_policy.tiers[].when.any_of”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.
lifetime_policy.tiers[].max_duration
Section titled “lifetime_policy.tiers[].max_duration”duration, default 0
The longest lifetime certificates in this tier can receive, bounded by the enclosing type’s valid_duration ceiling.
lifetime_policy.tiers[].grant_extensions
Section titled “lifetime_policy.tiers[].grant_extensions”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.
lifetime_policy.source_policy
Section titled “lifetime_policy.source_policy”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.
lifetime_policy.source_policy[].cidr
Section titled “lifetime_policy.source_policy[].cidr”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).