Skip to main content

Authentication policy

The authentication policy page collects the org-wide knobs that decide how your members authenticate to Git and the REST API. Each toggle is independent; defaults are the most permissive value so existing members aren’t surprise-locked-out when you first land on the page. Tighten them one at a time as your security posture matures.

Lives under Security › Access › Authentication policy (<your-org>/_admin/security/access/authentication; the older <your-org>/_admin/auth-policy redirects there). The page also opens with the two-factor authentication information that used to sit on the retired Edge policies page: two-factor authentication is always required, for every member.

2026-05-26 consolidation: the standalone SSH certificate authority tab was folded into this page as a sub-section under “Allow SSH protocol” (it only makes sense when SSH is enabled, so it appears in lockstep with the toggle). Direct links to /admin/ssh-ca/ now redirect here. The same write surfaces are preserved — paste CA keys, toggle require-cert — without leaving the auth-policy view.

Policies

SSH key constraints

When enabled, fremforge refuses new SSH key additions that aren’t hardware-backed. The OpenSSH key algorithm tells us — only sk-ssh-ed25519@openssh.com and sk-ecdsa-sha2-nistp256@openssh.com qualify; plain ssh-ed25519 and ssh-rsa are refused at the Add key form.

Hardware-backed means the private key lives in a security chip (macOS Secure Enclave, TPM, YubiKey, FIDO authenticator). The key can’t be extracted from the laptop. Touch ID / PIN / hardware tap is required on every push. This is the closest practical model to “MFA on git push” without a full SSH certificate authority setup.

Existing keys are NOT retroactively rejected — only new key additions are gated. Enabling this on a healthy org is safe; members can keep pushing with their existing keys, but adding a new laptop forces hardware-backed setup. Combine with SSH certificate authority below for the strongest end-state (no plain keys at all).

Audit-log actions: tenant.auth_policy.hardware_keys.enabled / tenant.auth_policy.hardware_keys.disabled.

SSH certificate authority

(Visible only when SSH protocol is enabled above — gating an SSH primitive on SSH-being-on is the natural reveal pattern.)

SSH certificate authentication is the strongest practical SSH sign-in path. A short-lived certificate issued by your own CA, bound to your IdP’s MFA flow, replaces the long-lived public key sitting on every developer laptop. fremforge ships native support for this model — the same primitive GitHub Enterprise Cloud calls “SSH certificate authority.”

Prerequisite: since 2026-05-22 fremforge defaults new orgs to SSH-disabled (the toggle above). If your org keeps the default, you don’t need SSH CA at all — the HTTPS + Git Credential Manager path gives you MFA-on-every-push via your IdP without any CA setup. If your org has explicitly re-enabled SSH (because you have a specific reason like legacy CI tooling or strong developer preference), this section is the next hardening layer for that opted-in SSH path.

When to use SSH CA

Use SSH certificate authentication if your org has re-enabled SSH AND any of the following apply:

  • You want every Git push to be gated by a fresh IdP-MFA challenge (no “lost laptop → attacker pushes for 90 days until the SSH key is noticed”)
  • You’re migrating from GitHub Enterprise Cloud and your developers expect the GHEC SSH CA flow
  • Your CISO has asked for “MFA on Git push” specifically AND insists on SSH transport — but note the cleaner answer is the default HTTPS+GCM path
  • You have an existing OpenSSH-issuing CA (Smallstep, HashiCorp Vault SSH secrets engine, custom OIDC-bound issuer) and want fremforge to trust certificates it signs

If you’re not in any of those situations, hardware-backed SSH keys (Touch ID, Secure Enclave, YubiKey — toggle above) are a simpler path with comparable security. See Secure sign-in for the choice matrix.

Setup

Step 1 — prepare your CA. Options ordered by ease:

  1. Smallstep step ca — open-source, EU-hosted-friendly, OIDC provisioner support out of the box. Bind it to your IdP and step ssh login becomes a one-line MFA flow.
  2. HashiCorp Vault SSH secrets engine — if you already run Vault for secrets, the SSH secrets engine adds OpenSSH cert issuance with policy-based access control.
  3. Manual ssh-keygen -s — proof-of-concept only.

Generate the CA key pair if you don’t have one:

ssh-keygen -t ed25519 -f acme-ssh-ca -C "acme-ssh-ca@example.com"

Produces acme-ssh-ca (private — keep secret, distribute to your issuing infrastructure) and acme-ssh-ca.pub (public — pasted into fremforge next).

Step 2 — register the CA. Paste acme-ssh-ca.pub into the Trusted CA public keys textarea on this page, one CA per line. Up to 8 CAs accepted (useful for rotation: add new, distribute new private key, retire old). Click Save CA keys. An automated platform job propagates the change into Forgejo’s SSH layer. Audit-log entry: tenant.ssh_ca.keys.updated.

Step 3 — have members register their principals. Each SSH certificate carries one or more principals, short strings that name who it is for. A certificate signed by a trusted CA signs a member in when one of its principals matches a principal that member registered on their own SSH keys settings page (the Manage SSH certificate principals section). fremforge accepts any principal string there, so issue certificates whose principal names the user unambiguously, for example their fremforge username.

Removed 2026-10-14: the “Principal policy” setting (Username / Email / Anything). It was saved but never enforced: Forgejo checks the shape of a principal against one platform-wide setting, not a per-org one, and a member can belong to several orgs. The API’s PUT /orgs/{org}/ssh-ca still accepts principal_policy so existing scripts and Terraform keep working, but ignores it, and GET no longer returns it.

Step 4 — optionally require cert auth. Once at least one CA is configured, toggle Require CA-signed certificate to refuse plain SSH public-key authentication. This is the cleanest end-state — long-lived ~/.ssh/id_* keys stop working entirely, every Git push must transit your CA (and thus your IdP’s MFA flow). Enabling this without a working CA configured would lock everyone out; the form refuses the change when no CA is configured. Audit-log entries: tenant.ssh_ca.require_cert.enabled / tenant.ssh_ca.require_cert.disabled.

Step 5 — issue users their first certificate. For Smallstep:

step ssh login user@example.com --provisioner=your-oidc-provisioner
# Browser opens → IdP login + MFA → cert issued + cached ~1 hour.

git clone git@frem.sh:<org>/<repo>.git
# OpenSSH presents the cert; fremforge checks the trusted CA and the user's registered principals.

For Vault, see the HashiCorp Vault SSH secrets engine docs. For manual ssh-keygen -s (POC only): ssh-keygen -s acme-ssh-ca -I "alice-cert-2026-05-22" -n alice -V +1h alice-id_ed25519.pub → distribute alice-id_ed25519-cert.pub to the user alongside their private key; OpenSSH picks it up automatically.

Rotating the CA

To replace an existing CA without an outage: generate a new CA key pair → add the new public key to the textarea alongside the existing one (up to 8 accepted) → update your CA infrastructure to issue with the new key → wait for old-CA-issued certs to expire (typically 1 hour) → remove the old CA from the textarea.

Disabling CA auth

Remove all CA keys from the textarea and save. Audit-log entry tenant.ssh_ca.keys.updated records next_key_count: 0. If Require CA-signed certificate was on, it auto-disables (the toggle requires at least one CA). Members go back to plain SSH public-key authentication.

PAT lifetime

Maximum allowed lifetime for newly-minted personal access tokens. Existing tokens are NOT retroactively shortened — the cap applies only at issuance.

SettingWhen to use
No limitForgejo default. Members set their own expiry (or none).
7 daysStrongest credential-rotation posture. Tokens become disposable.
30 daysCommon security-conscious default. Members refresh monthly.
90 daysBalance between rotation and friction. GHEC and GitLab default.
180 / 365 daysCI bots that need a stable identity but with annual rotation.

Recommended: 90 days as a starting point. Tighten to 30 if you have an active credential-scanning programme that catches leaked tokens within rotation windows.

Audit-log action: tenant.auth_policy.max_pat_lifetime.updated.

Allow repo deploy keys

Repo deploy keys are long-lived SSH credentials scoped to a single repository — a parallel long-lived-machine-creds path to user PATs. Useful for external CI / mirror clients that can’t federate via OIDC. Default: off — matches the secure-by-default posture of ssh_disabled=true and allow_user_pats=false. Long-lived machine credentials are off until you opt in.

When the policy is off (default), three layers fire:

  1. UI banner inside Forgejo — when a repo admin opens Repo settings → Deploy keys, the page is forwarded through api-intercept which injects a Fomantic-styled banner above the standard Forgejo header explaining that deploy keys are disabled by org policy. Forgejo’s nav + sidebar are preserved (the user is never bounced out of the repo). The Add deploy key form submission is blocked at api (POST is 303-redirected back to the GET so the user sees the banner explanation).
  2. REST API block — POST /api/v1/repos/<owner>/<repo>/keys returns 403 with error: use_fremforge_admin and redirect_to pointing here. DELETE and GET always forward (operators can clean up + audit existing keys regardless of policy).
  3. Soft-warn audit — every existing active deploy key emits a tenant.auth_policy.deploy_key_violation audit event on the next enforcer sweep. Existing keys are NOT auto-revoked; the operator decides which to drop via Forgejo repo settings (the delete path stays open).

When to enable: only for the niche case of external CI / mirror clients that genuinely can’t OIDC-federate. The intended alternative for CI machine identity is runner OIDC federation — short-lived tokens minted from fremforge’s OIDC issuer, federated to your cloud’s trust policy, no long-lived secret per repo. Most CI integrations don’t need deploy keys.

Audit-log actions: tenant.auth_policy.allow_repo_deploy_keys.enabled / .disabled (toggle); tenant.auth_policy.deploy_key_violation (per active key while policy is off).

Require signed commits

When enabled, the default branch on every repository in this org carries a branch-protection rule requiring signed commits. Members must configure GPG or SSH commit signing on their machines; unsigned commits to the default branch are rejected at push time by Forgejo.

The setting takes effect as soon as you save it. fremforge starts applying it to the org’s existing repositories at once, in the background, and the line under the toggle in Security › Policies › Repository rules shows the progress and when it finished. An hourly check keeps every default branch in line afterwards, also for repositories created later. Saving the same value again starts a new check.

Turning it off switches the requirement back off, again at once, on the rules fremforge turned on for this setting. A rule where a repository admin required signed commits themselves is left as it is, and so is the rest of every rule: approvals and status checks are not touched, and no rule is deleted. If an admin re-ticked “require signed commits” by hand on a rule fremforge had already set, fremforge cannot tell the two apart; that rule is switched off with the others, and the admin can set it again.

It is one setting with the signed-commits default for new repositories: saving it sets both, so a new repository starts with the same requirement (see Repository rules).

The toggle is the org-level floor — per-repo branch-protection overrides apply if set.

Through the API, PATCH /orgs/{org}/auth-policy with require_signed_commits does the same and answers signed_commits_applying: true.

Members signing setup:

# Generate a signing key (or use the existing SSH key)
ssh-keygen -t ed25519 -f ~/.ssh/git-signing-key -C "alice signing key"

# Configure git to sign with it
git config --global user.signingkey ~/.ssh/git-signing-key.pub
git config --global gpg.format ssh
git config --global commit.gpgsign true

# Upload the public key to frem.sh/user/settings/keys with usage:
# "Signing key" alongside the same key as an "Authentication key".

Audit-log actions: tenant.auth_policy.signed_commits.enabled / tenant.auth_policy.signed_commits.disabled (the setting); tenant.auth_policy.signed_commits.applied / tenant.auth_policy.signed_commits.reverted (per repository rule fremforge changed).

Allow SSH protocol

This toggle controls whether Git over SSH is permitted at all. Off (default for orgs created after 2026-05-22) means every SSH Git operation on the org’s repositories is refused: push, clone and fetch, and Git LFS over SSH. Clone and fetch are refused before Git starts; a push is refused at the pre-receive hook. A refused clone or fetch shows a message that names this setting and gives the HTTPS clone URL to use instead (see What a refused developer sees). Members use HTTPS + Git Credential Manager, which carries MFA through your org’s IdP on every token refresh. Turn the toggle on to allow SSH (the legacy, pre-2026-05-22 default).

Why this exists: the IP allowlist does not cover Git over SSH. SSH reaches fremforge through a load balancer that replaces the source address with one of our own cluster nodes, so the address your developer connects from is not visible to us, and fremforge refuses to judge a connection by an address it knows is wrong. Orgs that want every Git operation filtered by IP turn SSH off here and use HTTPS, which the allowlist covers.

What a refused developer sees

A git clone or git fetch over SSH on a repository of an org with SSH off fails with:

$ git clone git@frem.sh:acme/web.git
Cloning into 'web'...
Forgejo: SSH is turned off for the organisation acme (Authentication policy: "Allow SSH protocol" is off), so SSH push, clone and fetch are refused. Use HTTPS instead: git clone https://frem.sh/acme/web.git

fatal: Could not read from remote repository.

The developer’s SSH keys and their org membership are not touched: turning the toggle back on restores SSH access immediately.

Every refusal is recorded in the audit log as git.ssh.tenant_disabled_blocked, for pushes and for clone and fetch alike.

Audit-log actions: tenant.auth_policy.ssh_disabled.enabled / tenant.auth_policy.ssh_disabled.disabled.

IP allowlist

The IP allowlist moved to its own page: IP allowlist. One list now covers every HTTPS surface, so it no longer has a scope to choose. Its audit-only preview, runner toggle and break-glass are described there.

Audit log retention

Controls the queryable payload window of the audit log. The cryptographic chain tier (3-year WORM) is unaffected by this setting; only the readable actor + fields_json columns move.

PresetWhen to use
90 daysfremforge default. Aligns with DPA Annex A.7 baseline.
180 daysCompromise between audit-readiness and table size.
365 daysEnterprise plan default. DORA Art. 22 / NIS2 / BSI C5 alignment.
730 daysMaximum. Beyond this, queue quarterly Data exports and archive in your SIEM.

The setting applies at the daily redaction sweep (~02:30 UTC). Extending the window keeps newly-emitted rows queryable longer; shortening it irreversibly redacts rows older than the new window on the next sweep. See Audit log → Retention for the full mechanics.

Audit-log action: tenant.auth_policy.audit_retention.updated.

How they interact

GoalSettings
“GHEC-parity for MFA on Git”Default: SSH disabled + HTTPS-only via the pre-registered GCM OAuth app → MFA via your IdP fires on every push refresh.
“Hardware-only fleet”(If SSH is re-enabled for the org) Require hardware-backed SSH keys → max PAT lifetime = 30 days.
“MFA on every Git push, SSH transport”(If SSH is re-enabled for the org) Configure SSH CA → require CA-signed cert → bind CA to your IdP’s MFA flow.
“No long-lived machine credentials”Disable user PATs → disable repo deploy keys → use runner OIDC federation for all CI. Pair with require-cert-auth above if SSH stays on.
“Audit before enforcing”Set the IP allowlist with audit-only preview on → review ip_allowlist.would_block events → turn audit-only off
“Customer-handed-the-CISO-screenshot”SSH disabled (HTTPS+GCM only) + max PAT lifetime 30d + deploy keys disabled + signed commits required + IP allowlist with audit-only off after burn-in

What enforces what

These policies are enforced at the platform layer on every request. The admin UI is the source of truth for what the org has chosen.

Limits

  • The PAT lifetime cap applies at issuance. Existing tokens are NOT retroactively shortened. If you tighten the cap, expect some bot/CI tokens to outlive the new policy; they’ll still work until their original expiry.
  • Hardware-key enforcement only applies to NEW key additions. Members keep existing keys.
  • Signed commits are applied to existing repos when the setting is saved, in the background; a large org takes a minute or two, and the progress shows under the toggle. New repos created after the toggle is on get the rule at creation.