Skip to content

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.

Key Type Default
cert_options.service.require_group string empty
cert_options.service.require.group string empty
cert_options.service.require.claim string empty
cert_options.service.require.at_least number 0
cert_options.service.require.at_most number 0
cert_options.service.require.exactly number 0
cert_options.service.require.equals string empty
cert_options.service.require.one_of list empty
cert_options.service.require.contains string empty
cert_options.service.require.all_of list empty
cert_options.service.require.any_of list empty
cert_options.service.allow_user_accounts bool false
cert_options.service.valid_duration duration 8760h
cert_options.service.extensions list empty
cert_options.service.enrollment_duration duration 8760h
cert_options.service.key_id_template string empty
cert_options.service.lifetime_policy.default_duration duration 0
cert_options.service.lifetime_policy.on_absent_claim string empty
cert_options.service.lifetime_policy.default_extensions list empty
cert_options.service.lifetime_policy.default_enrollment_duration duration 0
cert_options.service.lifetime_policy.tiers list empty
cert_options.service.lifetime_policy.tiers[].name string empty
cert_options.service.lifetime_policy.tiers[].when.group string empty
cert_options.service.lifetime_policy.tiers[].when.claim string empty
cert_options.service.lifetime_policy.tiers[].when.at_least number 0
cert_options.service.lifetime_policy.tiers[].when.at_most number 0
cert_options.service.lifetime_policy.tiers[].when.exactly number 0
cert_options.service.lifetime_policy.tiers[].when.equals string empty
cert_options.service.lifetime_policy.tiers[].when.one_of list empty
cert_options.service.lifetime_policy.tiers[].when.contains string empty
cert_options.service.lifetime_policy.tiers[].when.all_of list empty
cert_options.service.lifetime_policy.tiers[].when.any_of list empty
cert_options.service.lifetime_policy.tiers[].max_duration duration 0
cert_options.service.lifetime_policy.tiers[].grant_extensions list empty
cert_options.service.lifetime_policy.tiers[].max_enrollment_duration duration 0
cert_options.service.lifetime_policy.source_policy list empty
cert_options.service.lifetime_policy.source_policy[].cidr string empty
cert_options.service.lifetime_policy.source_policy[].max_duration duration 0
cert_options.service.lifetime_policy.source_policy[].extensions list empty
cert_options.service.lifetime_policy.source_policy[].removed_extensions list empty
cert_options.service.lifetime_policy.source_policy[].pin_source_address bool false

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: ""

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.

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.

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: false

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: 8760h

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: []

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: 8760h

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: ""

Configures tiered certificate duration based on OIDC group membership and source network narrowing — see https://mnestor.github.io/ssoossh/operations/certificate-policy/.

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:
service:
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).