BetterIAM

SSH access

Make Better IAM the OpenSSH certificate authority for your servers, with short-lived user certificates decided by policy, host certificates, and revocation.

new@better-iam/server@better-iam/clissh-access.mdssh.tsssh-ca.tsssh.tsssh.ts

Most teams manage SSH access by copying public keys into authorized_keys files on every server. Those keys never expire, nobody knows exactly which servers hold a given person's key, and when someone leaves, their key stays on every server nobody remembered to clean up. Nor is there one place to answer "who can log in to the database hosts as postgres?".

OpenSSH has a better model built in: certificates. A server that trusts a certificate authority (CA) accepts any key the CA signed, for the principals the certificate names, until the certificate expires. Better IAM can be that authority. People and machines sign in as usual and get a short-lived OpenSSH certificate for exactly the hosts and local accounts their allow. Servers run stock sshd, a few files under /etc/ssh, and a sync job; nothing else.

  • User certificates name each host and login they open. They last hours, never longer than the that asked for them.
  • Host certificates let clients verify servers by certificate, so there are no trust-on-first-use prompts.
  • Revocation works through key revocation lists (KRLs) that servers fetch every few minutes. A sweep revokes certificates whose holder or access went away.
  • Policies decide, as everywhere else. Getting a certificate is an ssh:login decision, so , , just-in-time roles, access windows, delegation scopes and access reviews all apply.

Who configures it. Your team turns on the ssh option and schedules the sweep job. An organization's administrator creates its authorities and registers hosts, whoever runs a server enrolls it with a one-time join token, and members fetch certificates with the CLI or your own UI.

Turn it on

The certificate authority is off until the deployment sets the ssh option. While it is off, every method of the ssh API group fails with FEATURE_DISABLED.

Enable the option

iam.ts
import { betterIam } from 'better-iam';

export const iam = betterIam({
  // database, secret, baseURL, ...
  ssh: true, // or an options object, see Options below
});

Schedule the sweep

Run iam.ssh.sweep() (or better-iam ssh-sweep) every few minutes. It revokes certificates whose holder, session or access went away; see Revocation.

jobs.ts
import { iam } from './iam';

setInterval(() => void iam.ssh.sweep(), 5 * 60_000);

Create the authorities

An administrator of each organization calls ssh.setup once, then registers hosts. See Set up the authority and hosts.

The option adds the ssh-login and ssh-host resource types and their actions to the permission catalog. Its storage collections are created by the ordinary migration (iam.initialize() or better-iam migrate), so turning it on needs no extra migration step.

Options

ssh: true uses every default. An object sets any of these fields; a value outside its range fails with INVALID_CONFIG when betterIam() starts.

OptionTypeDefaultDescription
maxUserCertificateMsnumber24 hoursThe longest user certificate any tenant may allow, 5 minutes to 7 days. Tenant settings can only go lower.
hostCertificateMsnumber90 daysHost certificate lifetime, 1 day to 1 year. Hosts renew with their renewal token.
clockSkewMsnumber5 minutesHow far the start of validity ("valid after") is backdated against clock skew, at most 1 hour.
maxHostsPerCertificatenumber64The most hosts one user certificate may name, 1 to 256.
recordRetentionDaysnumber90How long certificate records stay after they expire, for audits (1 to 3650).
joinTokenMsnumber24 hoursHow long a host join token stays valid, 5 minutes to 30 days.
issuanceLimitnumber60Certificates one identity may request per rate-limit window.

Per-organization rules (MFA, security keys, lifetimes, host patterns) are tenant settings, not deployment options.

How access is decided

Every enrolled host lists the local accounts people may use on it (logins). Every pair of host and login is a resource of type ssh-login, named ssh-login/{host}/{login}, whose attributes are the host's labels plus host and login. Forwarding is decided on the host itself, ssh-host/{host}:

ActionResourceGrants
ssh:loginssh-login/{host}/{login}Logging in to the host as that login
ssh:port-forwardssh-host/{host}The permit-port-forwarding extension
ssh:agent-forwardssh-host/{host}The permit-agent-forwarding extension
ssh:x11-forwardssh-host/{host}The permit-X11-forwarding extension

Certificate extensions apply to every host a certificate names, so a certificate carries a forwarding extension only when every host it names allows that kind of forwarding. Every certificate carries permit-pty and permit-user-rc.

Deploy to staging, reach databases with MFA, forward ports anywhere
{
  "version": 1,
  "statements": [
    {
      "effect": "allow",
      "actions": ["ssh:login"],
      "resources": ["ssh-login/*/deploy"],
      "conditions": { "StringEquals": { "resource.environment": "staging" } }
    },
    {
      "effect": "allow",
      "actions": ["ssh:login"],
      "resources": ["ssh-login/db-*/postgres"],
      "conditions": { "Bool": { "principal.mfa": true } }
    },
    { "effect": "allow", "actions": ["ssh:port-forward"], "resources": ["ssh-host/*"] }
  ]
}

A certificate carries one principal per allowed pair, {login}@{host}. On each host, the principals file of each login holds only that host's own principal, so a certificate opens nothing it was not issued for. A host that was not set up with a principals file refuses these certificates outright, because a principal such as deploy@web-01 never equals a local user name.

The same decisions work through the generic API, so reviews and tests need nothing SSH-specific: iam.authorize({ token, tenantId, action: 'ssh:login', resource: { type: 'ssh-login', id: 'web-01/deploy' } }), policies.whoCan, policies.simulate, and ssh.whoCanLogin for one host. whoCanLogin (iam:ssh:read) decides each active identity in a (with MFA when assumeMfa), optionally for one login or identity kind. A host or login that does not exist is NOT_FOUND.

Two principals never get certificates:

  • "View as" sessions. An session is refused (IMPERSONATION_RESTRICTED).
  • The platform root override in another organization. A administrator needs a role in the organization like anyone else (ROOT_SSH_RESTRICTED). The root tenant's own hosts are the exception.

Set up the authority and hosts

An administrator with iam:ssh:manage creates the tenant's two authorities, one that signs user certificates and one that signs host certificates, and then registers hosts. Hosts can be registered only once the authorities exist (SSH_NOT_CONFIGURED otherwise). setup is idempotent: a second call creates nothing.

await iam.api.ssh.setup(admin, { tenantId });

const { host, joinToken } = await iam.api.ssh.createHost(admin, {
  tenantId,
  name: 'web-01',
  addresses: ['web-01.corp.example.com', '10.0.0.5'],
  logins: ['deploy', 'ubuntu'],
  labels: { environment: 'staging', team: 'payments' },
});

Routes: POST /api/iam/ssh/setup, POST /api/iam/ssh/createHost

Prop

Type

createHost returns the host and a one-time join token, shown once and valid for joinTokenMs.

Names the organization can vouch for

The host certificate names the host's name and every address, and clients trust the host authority for them. The authority must therefore never vouch for a name the organization does not own:

  • Owned names. Without hostPatterns (see tenant settings), a name or address must be a single label (web-01), a private address (10.0.0.5, fd00::5), or a name under one of the organization's verified domains. With hostPatterns, it must also match them, and a public IP address needs a pattern that fixes its prefix (203.0.113.*). Anything else is refused with HOST_OUTSIDE_PATTERNS.
  • Unique names. No two hosts of the organization share a name or address. Reusing another host's name is CONFLICT; any other overlap is HOST_NAME_TAKEN.
  • Names within your scope. iam:ssh:manage is decided on iam/ssh/hosts/{name} for the host's name and for every address, so an administrator allowed only iam/ssh/hosts/team-a-* cannot give a host the address payroll.acme.com.

updateHost changes addresses, logins, labels or the description (null clears it); new addresses follow the same rules. Removing an address revokes the host's certificate at once, and the host gets a new one at its next sync. New logins and addresses reach the server at its next sync too (syncRequired in the result says so). Refusals are audited as denied iam:ssh:manage events with the code in metadata.reason.

disableHost takes a host out of service: its certificate is revoked, its key is published to clients as revoked, its tokens stop working, and no user certificate names it any more. deleteHost removes it and revokes its certificate.

Enroll a server

Run the enrollment on the server. The CLI reads the server's public host key (--host-key, default /etc/ssh/ssh_host_ed25519_key.pub) and sends it with the join token. The token works once.

BETTER_IAM_SSH_JOIN_TOKEN=biam_sshj.... better-iam ssh-host-enroll --url https://iam.example.com

--dry-run lists the files without writing them, and --root DIR writes under another directory (for images and tests).

Check what it wrote. enrollHost returns everything sshd needs, and the CLI writes it (only ever under /etc/ssh):

FileFor
/etc/ssh/ssh_host_ed25519_key-cert.pubHostCertificate, signed by the host authority (named after the host key type)
/etc/ssh/better-iam/user-ca.pubTrustedUserCAKeys
/etc/ssh/better-iam/principals/{login}AuthorizedPrincipalsFile, holding {login}@{host}
/etc/ssh/better-iam/revoked.krlRevokedKeys
/etc/ssh/sshd_config.d/00-better-iam.confThe drop-in naming the four above
/etc/ssh/better-iam/renewal-tokenThe host's credential for syncing (mode 0600)
/etc/ssh/sshd_config.d/00-better-iam.conf
# Managed by better-iam: host web-01 of tenant ten_123
# The first value sshd reads wins, so this drop-in sorts first (check with sshd -T).
TrustedUserCAKeys /etc/ssh/better-iam/user-ca.pub
AuthorizedPrincipalsFile /etc/ssh/better-iam/principals/%u
RevokedKeys /etc/ssh/better-iam/revoked.krl
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub

sshd uses the first value it reads for each setting, so the drop-in's name sorts first. sshd reads it only when sshd_config includes /etc/ssh/sshd_config.d/*.conf; check the effective configuration with sshd -T.

Reload sshd, for example with systemctl reload ssh.

Schedule the sync. Run better-iam ssh-host-sync --url https://iam.example.com every few minutes from cron or a systemd timer. Revocations and principals apply to the next login without a reload; reload sshd when a sync renews the certificate (certificateRenewed) or the trust changed.

Each sync fetches the current trust, principals files and revocation list. It issues a new host certificate when the current one is two thirds through its life, the host's addresses changed, or the host authority rotated; renew: true (--renew) forces one, at most hourly. A new certificate comes with a new renewal token, which the CLI saves. The previous token keeps working until the new one is used, so a lost response never locks a host out. The CLI also deletes the principals files of logins the host no longer has. Enrollment and syncs are rate limited per host.

Without the CLI, call the public routes yourself. Like every POST to the handler, they need Content-Type: application/json and X-Better-IAM: 1:

curl -s https://iam.example.com/api/iam/ssh/syncHost \
  -H 'content-type: application/json' -H 'x-better-iam: 1' \
  -d "{\"renewalToken\":\"$(cat /etc/ssh/better-iam/renewal-token)\"}"

A renewal token never moves a host to another key

A sync that presents a different host key fails with HOST_KEY_CHANGED (audited as a denied ssh:host:sync), so a stolen renewal token cannot produce a host certificate for an attacker's key. After rebuilding a server, re-enroll it with a new join token from ssh.resetJoinToken, which also invalidates its renewal token.

Every certificate a host held before is revoked as superseded whenever it gets a new one, and a key that no enrolled host uses any more is published to clients as revoked. A host key cannot be a security key or one of the organization's authority keys (INVALID_INPUT), nor another host's key (HOST_KEY_IN_USE), so revoking one host never revokes anything else.

While the organization is suspended, hosts keep syncing but get no new certificates, and their revocation list revokes every user certificate. Enrolling needs an active organization (TENANT_INACTIVE).

Get a certificate

People, and or agents with their API keys, call issueCertificate, or run the CLI:

better-iam login --url https://iam.example.com
better-iam ssh-cert --tenant ten_123                 # every host and login you may use
better-iam ssh-cert --hosts web-01 --logins deploy --ttl-minutes 30 --reason "hotfix 42"
ssh deploy@web-01.corp.example.com

ssh-cert reads your public key (--key, default ~/.ssh/id_ed25519.pub) and saves the certificate next to it (~/.ssh/id_ed25519-cert.pub), where ssh finds it. It also writes the host authority, for the organization's host names, to ~/.ssh/better-iam_known_hosts (--known-hosts to change), and the host revocation list beside it as ~/.ssh/better-iam_revoked_hosts. --no-write prints the certificate instead. Add both files to your SSH configuration once:

~/.ssh/config
Host *.corp.example.com web-* db-*
  UserKnownHostsFile ~/.ssh/known_hosts ~/.ssh/better-iam_known_hosts
  RevokedHostKeys ~/.ssh/better-iam_revoked_hosts

clientTrust returns the same two for a session of the organization. The public trust route gives only the authority keys (and the @cert-authority line for configured hostPatterns), so the host inventory is never public.

const cert = await iam.api.ssh.issueCertificate(session, {
  tenantId,
  publicKey: 'ssh-ed25519 AAAA... alice@laptop',
  hosts: ['web-01'], // optional; default: every enrolled host you may open
  logins: ['deploy'], // optional
  ttlMs: 30 * 60_000, // optional; default and maximum from the tenant settings
  reason: 'hotfix 42', // optional; audited
});
// cert.certificate: 'ssh-ed25519-cert-v01@openssh.com AAAA...'
// cert.principals: ['deploy@web-01'], cert.validBefore, cert.hosts, cert.knownHosts, cert.revokedHostKeys

Route: POST /api/iam/ssh/issueCertificate

A request names at most 256 hosts and 64 logins, and a certificate at most maxHostsPerCertificate hosts and 256 principals (TOO_MANY_HOSTS otherwise; name fewer with hosts). A named host that is not enrolled is NOT_FOUND.

myAccess lists the hosts, logins and forwarding the caller may use, with the settings that affect a request (like tsh ls), and myCertificates lists their last 100 certificates. Anyone may revoke their own user certificate with revokeCertificate without a permission. A "view as" session or a credential narrowed by a session policy needs iam:ssh:manage for that, and an agent acting for a person may revoke only the certificates its own session obtained.

How long a certificate lives

A certificate's lifetime is the requested ttlMs (at least one minute), or the tenant's default, capped by all of:

  • the tenant's maximum, and the account's expiry;
  • the session that asked for it. A certificate lives only as long as that session: signing out, revoking the person's sessions, revoking the API key, or the session's idle timeout ends it at the next revocation list or sweep;
  • the earliest end of any time-limited grant that can allow ssh:login: a just-in-time , an expiring binding or group membership, or an . Standing grants set no limit;
  • ten minutes for agents acting for people (delegated sessions).

When the result is less than a minute away, the request fails with SESSION_EXPIRING: renew the session or the access first.

Issuing is rate limited per identity (issuanceLimit, 60 per window by default). The key ID that sshd logs is {email} ({identityId}), plus via agent {id} for delegated sessions. Supported keys are Ed25519, ECDSA (P-256/384/521), RSA of 2048 bits or more, and FIDO security keys (ed25519-sk, ecdsa-sk).

Issuing is audited as ssh:certificate:issue, with the serial, key ID, hosts, fingerprint and reason, and counts as using ssh:login for usage tracking. A refusal by policy or by the tenant's rules (access denied, MFA, security key, "view as", root override) is a denied ssh:login event whose metadata.reason names the code.

Tenant settings

updateSettings (iam:ssh:manage) changes a tenant's rules; fields left out keep their values. getSettings (iam:ssh:read) reads them, with the deployment's ceiling as deploymentMaxCertificateMs.

Prop

Type

bindSourceAddress needs the client IP, which the handler knows only when you supply http.clientInfo.

Without hostPatterns, members' clients trust the host authority for exactly the enrolled hosts' names and addresses, never for *. With patterns such as ['*.corp.example.com', 'web-*', '10.20.*', '!bastion.corp.example.com'] (* and ? globs, ! negation), every host name and address must match them as well, and clients trust the authority for the patterns. Each positive pattern must end in a verified domain of the organization, fix an address prefix, or be a short single-label name; * alone is refused, at least one pattern must be positive, and the patterns must cover every host already enrolled. The authority can then never vouch for a name like github.com, even if an administrator enters it.

Revocation

There are two key revocation lists, both from revocationList:

  • user (the default), which hosts load as RevokedKeys and enforce from their next sync. It holds:
    • certificates revoked by hand: revokeCertificate, revokeIdentity (everything one identity holds), and revokeAllCertificates (the whole tenant, for a suspected compromise);
    • certificates of people and machines who are no longer active, or whose session is gone, even before the sweep marks them;
    • every user certificate while the organization is suspended.
  • host (kind: 'host'), which clients load as RevokedHostKeys. It holds revoked host certificates (of disabled, deleted and re-enrolled hosts, and after an address was removed) and host keys no enrolled host uses any more. Members also get these keys as @revoked lines in their known_hosts file.

Schedule the sweep

Run iam.ssh.sweep() or better-iam ssh-sweep every few minutes. It re-checks every live user certificate against the very session that requested it, with the same validation every request gets, and revokes certificates whose holder is no longer active (identity-inactive), whose session was revoked, expired or no longer validates (session-ended), or whose hosts, logins or forwarding policies no longer allow (access-changed, which also covers a login removed from a host). Removing a binding therefore takes SSH access away within minutes, not when the certificate expires. ssh.sweep (iam:ssh:manage) runs it for one tenant on demand.

Each revocation by the sweep is audited as ssh:certificate:revoke with the reason. revocationList is public: like an X.509 CRL, it holds serials and public keys only. It is read outside any transaction and cached for 10 seconds per tenant, kind and revocation version, and every revocation bumps the version. iam.ssh.revocationList(tenantId, kind) returns the same list as bytes, for serving it from a custom route.

Rotate an authority

Rotation is two-phase, so no certificate stops working unexpectedly:

rotateAuthority({ kind: 'user' }) (or 'host') publishes a new pending key. Hosts and clients trust it from their next sync. Only one rotation per kind may be pending (CONFLICT).

activateAuthority({ authorityId }) makes it sign. The old key stays trusted as previous, so the certificates it signed keep working until they expire.

retireAuthority({ authorityId }) stops trusting the previous key once its certificates have expired. It is refused with RESOURCE_IN_USE while valid certificates remain, unless you pass force: true, which revokes them.

After a compromise, rotateAuthority({ kind, activate: true }) switches at once. User certificates then work only on hosts that have synced since. A host authority rotation re-issues host certificates at each host's next sync.

Authority private keys are Ed25519, sealed with the deployment secret, and re-sealed by iam.rotateSecrets() (see rotating the deployment secret). No API response ever contains them; listAuthorities shows public keys and fingerprints only.

API and permissions

Administration is decided on iam/ssh/... resources: iam/ssh/authorities/{kind}, iam/ssh/hosts/{name}, iam/ssh/certificates/{id}, iam/ssh/identities/{identityId} and iam/ssh/settings.

MethodAccess
setup, updateSettings, rotateAuthority, activateAuthority, retireAuthorityiam:ssh:manage
createHost, updateHost, disableHost, resetJoinToken, deleteHostiam:ssh:manage
revokeCertificate (someone else's), revokeIdentity, revokeAllCertificates, sweepiam:ssh:manage
status, getSettings, listAuthorities, getHost, listHosts, listCertificates, getCertificate, whoCanLoginiam:ssh:read
issueCertificate, myAccess, myCertificates, clientTrust, revokeCertificate (own)A session; ssh:login decides
enrollHost (join token), syncHost (renewal token), trust, revocationListPublic

The codes these methods fail with are listed under Errors.

Audit events: ssh:certificate:issue, ssh:certificate:revoke (sweep and self-service), ssh:host:enroll, ssh:host:renew, denied ssh:host:enroll and ssh:host:sync (a real host's token refused, a key swap), denied ssh:login, and the iam:ssh:* administration operations (denied ones name the refusal in metadata.reason).

Routes

Every method is a POST under the handler's base path (default /api/iam). Public routes need no credential; the others take a session cookie, bearer token or API key like any other method.

MethodRouteCredential
ssh.setupPOST /api/iam/ssh/setupRequired
ssh.statusPOST /api/iam/ssh/statusRequired
ssh.getSettingsPOST /api/iam/ssh/getSettingsRequired
ssh.updateSettingsPOST /api/iam/ssh/updateSettingsRequired
ssh.listAuthoritiesPOST /api/iam/ssh/listAuthoritiesRequired
ssh.rotateAuthorityPOST /api/iam/ssh/rotateAuthorityRequired
ssh.activateAuthorityPOST /api/iam/ssh/activateAuthorityRequired
ssh.retireAuthorityPOST /api/iam/ssh/retireAuthorityRequired
ssh.createHostPOST /api/iam/ssh/createHostRequired
ssh.getHostPOST /api/iam/ssh/getHostRequired
ssh.listHostsPOST /api/iam/ssh/listHostsRequired
ssh.updateHostPOST /api/iam/ssh/updateHostRequired
ssh.disableHostPOST /api/iam/ssh/disableHostRequired
ssh.resetJoinTokenPOST /api/iam/ssh/resetJoinTokenRequired
ssh.deleteHostPOST /api/iam/ssh/deleteHostRequired
ssh.enrollHostPOST /api/iam/ssh/enrollHostNone (join token)
ssh.syncHostPOST /api/iam/ssh/syncHostNone (renewal token)
ssh.issueCertificatePOST /api/iam/ssh/issueCertificateRequired
ssh.getCertificatePOST /api/iam/ssh/getCertificateRequired
ssh.listCertificatesPOST /api/iam/ssh/listCertificatesRequired
ssh.myAccessPOST /api/iam/ssh/myAccessRequired
ssh.myCertificatesPOST /api/iam/ssh/myCertificatesRequired
ssh.clientTrustPOST /api/iam/ssh/clientTrustRequired
ssh.whoCanLoginPOST /api/iam/ssh/whoCanLoginRequired
ssh.revokeCertificatePOST /api/iam/ssh/revokeCertificateRequired
ssh.revokeIdentityPOST /api/iam/ssh/revokeIdentityRequired
ssh.revokeAllCertificatesPOST /api/iam/ssh/revokeAllCertificatesRequired
ssh.sweepPOST /api/iam/ssh/sweepRequired
ssh.trustPOST /api/iam/ssh/trustNone
ssh.revocationListPOST /api/iam/ssh/revocationListNone

Schema

The ssh option stores four tenant-scoped collections. Authority private keys are stored sealed with the deployment secret (keySealed), and join and renewal tokens only as hashes.

CollectionOne record perKey fields
sshSettingsTenant (its id is the tenant ID)requireMfa, requireSecurityKey, requireUserVerification, bindSourceAddress, defaultCertificateMs, maxCertificateMs, hostPatterns, revocationVersion
sshAuthoritiesAuthority keykind (user or host), status (pending, active, previous, retired), algorithm (ssh-ed25519), publicKey, fingerprint, keySealed, activatedAt, retiredAt
sshHostsRegistered servername, addresses, logins, labels, status (pending, enrolled, disabled), hostKey, hostKeyFingerprint, certificateId, certificateExpiresAt, lastSeenAt
sshCertificatesIssued user or host certificatekind, serial, keyId, authorityId, identityId, sessionId, hostId, principals, access, extensions, validAfter, validBefore, revokedAt, revocationReason, expiresAt

revocationReason is one of revoked, identity-revoked, identity-inactive, access-changed, session-ended, host-disabled, host-deleted, addresses-changed, superseded, authority-retired and tenant-revoked. A certificate record's expiresAt is its validBefore plus recordRetentionDays; the retention worker deletes it after that.

Errors

CodeStatusWhen
FEATURE_DISABLED403The deployment did not set the ssh option.
INVALID_CONFIG400An ssh option value is outside its range.
SSH_NOT_CONFIGURED409A host was registered or a certificate requested before ssh.setup.
MFA_REQUIRED403The tenant sets requireMfa and the person's session has no MFA.
SECURITY_KEY_REQUIRED403The tenant sets requireSecurityKey and the key is not ed25519-sk or ecdsa-sk.
SOURCE_ADDRESS_UNKNOWN400The tenant sets bindSourceAddress and the server does not know the caller's IP.
TOO_MANY_HOSTS400The certificate would name more than maxHostsPerCertificate hosts or 256 principals.
SESSION_EXPIRING401The session or the time-limited access allowing ssh:login ends within a minute.
IMPERSONATION_RESTRICTED403A "view as" session asked for a certificate.
ROOT_SSH_RESTRICTED403A platform root administrator without a role in the organization asked for a certificate.
HOST_OUTSIDE_PATTERNS400A host name or address is outside the names the organization can vouch for.
HOST_NAME_TAKEN409Another host already uses one of the names or addresses.
HOST_KEY_IN_USE409Another enrolled host already uses this host key.
HOST_KEY_CHANGED409A sync presented a different host key than the enrolled one; re-enroll with a new join token.
INVALID_TOKEN401A join or renewal token is unknown, used, or expired.
TENANT_INACTIVE403Enrollment while the organization is suspended.
RESOURCE_IN_USE409retireAuthority while certificates it signed are still valid, without force: true.
CONFLICT409A duplicate host name, or a second pending rotation of the same authority kind.
NOT_FOUND404A named host, login, certificate or authority does not exist (or a host is not enrolled).
RATE_LIMITED429More than issuanceLimit certificates in a window, or too many enrollments and syncs.

SECURITY_KEY_REQUIRED and ROOT_SSH_RESTRICTED are thrown by ssh.issueCertificate but do not yet have entries in the error reference.

Next steps

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page