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 at <your-org>/_admin/auth-policy, under the Security group in the org admin sidebar.
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, set principal policy, toggle require-cert — without leaving the auth-policy view.
Policies
Require hardware-backed SSH keys
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:
- Smallstep
step ca— open-source, EU-hosted-friendly, OIDC provisioner support out of the box. Bind it to your IdP andstep ssh loginbecomes a one-line MFA flow. - 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.
- 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 — choose the principal policy. Each SSH certificate carries one or more principals — short strings that name the user(s) it’s valid for:
| Policy | Meaning | When to use |
|---|---|---|
| Username | Cert principal = fremforge username | Default. Use when your CA emits certs with username as principal. |
| Cert principal = one of the user’s verified emails | Use when your IdP issues certs with email as principal. | |
| Anything | Cert principal can be any string | Use when your IdP issues opaque user IDs. Trust shifts to the CA signature alone. |
Audit-log entry: tenant.ssh_ca.principal_policy.updated.
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 validates against trusted CA + principal policy.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.
Max PAT lifetime
Maximum allowed lifetime for newly-minted personal access tokens. Existing tokens are NOT retroactively shortened — the cap applies only at issuance.
| Setting | When to use |
|---|---|
| No limit | Forgejo default. Members set their own expiry (or none). |
| 7 days | Strongest credential-rotation posture. Tokens become disposable. |
| 30 days | Common security-conscious default. Members refresh monthly. |
| 90 days | Balance between rotation and friction. GHEC and GitLab default. |
| 180 / 365 days | CI 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:
- 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).
- REST API block —
POST /api/v1/repos/<owner>/<repo>/keysreturns 403 witherror: use_fremforge_adminandredirect_topointing here.DELETEandGETalways forward (operators can clean up + audit existing keys regardless of policy). - Soft-warn audit — every existing active deploy key emits a
tenant.auth_policy.deploy_key_violationaudit 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 toggle is the org-level floor — per-repo branch-protection overrides apply if set. Flipping it off does NOT remove existing per-repo rules; admins remove those manually if reverting.
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.
Disable SSH protocol
When enabled (default for orgs created after 2026-05-22), SSH-protocol Git push is rejected at the pre-receive hook with a customer-friendly message pointing to the Secure sign-in guide. Members use HTTPS + Git Credential Manager instead, which carries MFA through your org’s IdP on every token refresh.
Why this exists: SSH source IPs aren’t recoverable in fremforge’s current architecture (Forgejo’s built-in SSH server doesn’t propagate source IPs to enforcement hooks + the OTC load balancer SNATs SSH connections to cluster IPs). The IP allowlist scope below therefore covers web + HTTPS Git only. Orgs that want IP-based restriction on every Git operation switch to HTTPS-only here, then set the allowlist scope to web+git. Combined, every Git operation is IP-gated.
Caveat — SSH pull/clone: the kill switch enforces push only (pre-receive hook). Pull/clone via SSH still slips through because Forgejo’s built-in SSH server has no pre-upload-pack hook — the SSH audit watchdog logs observed sessions as git.ssh.tenant_disabled_observed audit events so admins have visibility. Pair with rotating members’ SSH keys when this matters.
Audit-log actions: tenant.auth_policy.ssh_disabled.enabled / tenant.auth_policy.ssh_disabled.disabled.
IP allowlist scope
Controls which surfaces the IP allowlist (configured on the Security & policies page) applies to. Independent setting — has no effect until you set at least one CIDR on the Security page.
| Scope | Surfaces gated |
|---|---|
web (default) | Admin UI + REST API |
web+git | Admin UI + REST API + HTTPS Git push/pull |
The previous all scope (intended to cover SSH push/pull) was removed in 2026-05-22 because SSH source IPs aren’t enforceable in fremforge’s current architecture (see the “Disable SSH protocol” section above). For an SSH-side restriction, disable SSH at the toggle above + steer developers to HTTPS+GCM via the Secure sign-in guide.
Audit-log action: tenant.auth_policy.ip_allowlist_scope.updated.
IP allowlist audit-only (preview mode)
When enabled, IP-allowlist violations are logged but not blocked. The audit log records each would-be denial; requests still go through.
Use this to observe the impact of a new allowlist (or a scope-widening change) before flipping it to hard-block. Pattern:
- Configure the CIDR list on the Security page
- Set scope to
web+git(the only value beyondwebsince 2026-05-22) - Enable audit-only mode
- Wait 24-48 hours
- Review
tenant_status_blockedandip_allowlist_blockedevents in the audit log - Adjust CIDRs to cover any legitimate traffic that would have been refused
- Disable audit-only — the allowlist now hard-blocks
Audit-log actions: tenant.auth_policy.ip_allowlist_audit_only.enabled / tenant.auth_policy.ip_allowlist_audit_only.disabled.
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.
| Preset | When to use |
|---|---|
| 90 days | fremforge default. Aligns with DPA Annex A.7 baseline. |
| 180 days | Compromise between audit-readiness and table size. |
| 365 days | Enterprise plan default. DORA Art. 22 / NIS2 / BSI C5 alignment. |
| 730 days | Maximum. 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
| Goal | Settings |
|---|---|
| “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 up IP allowlist + scope=web+git → enable audit-only → review denials → disable audit-only |
| “Customer-handed-the-CISO-screenshot” | SSH disabled (HTTPS+GCM only) + max PAT lifetime 30d + deploy keys disabled + signed commits required + IP allowlist scope web+git + audit-only off after burn-in |
What enforces what
| Policy | Where enforcement runs (today + planned) |
|---|---|
| Hardware-backed SSH keys | At Forgejo’s /-/user/settings/keys add-key form. Implemented in the SSH-key-add hook (Workstream C). |
| SSH CA — trusted CA keys | Stored on tenant_security_policies.ssh_ca_keys_json. A platform job propagates the list into Forgejo’s SSH layer (TrustedUserCAKeys per app.ini). |
| SSH CA — principal policy | Enforced at SSH connect time by the platform-side cert-validation pre-receive shim that maps the cert’s principals against the configured policy. |
| SSH CA — require cert auth | Same shim — when on, plain pubkey auth is refused before reaching git-receive-pack. |
| Max PAT lifetime | At PAT mint time (PAT-mint wizard, Workstream C). |
| Allow repo deploy keys | Default off (migration 0154). Bunny edge rules route /<owner>/<repo>/settings/keys* + /api/v1/repos/*/*/keys* to forgejo-intercepts.tsx, which checks allow_repo_deploy_keys. UI GET → forward to Forgejo with Fomantic banner injected (preserves Forgejo nav/sidebar); UI POST → 303 back to GET; REST API POST → 403 JSON; DELETE/GET always forward. Hourly enforcer sweep emits deploy_key_violation events for active keys when policy is off. |
| Signed commits | Platform job watches this column and writes branch-protection rules to Forgejo per repo (Workstream C). |
| IP allowlist scope | tenantStatusMiddleware (/_app/middleware/tenant-status.ts) extended to read scope + cover the appropriate paths (Workstream A). |
| Audit-only mode | Same middleware — when set, runs the CIDR check but doesn’t block. |
Toggles persist in the database today; enforcement code lands per-toggle in follow-up workstream slices. The admin UI is the source of truth for what the customer has chosen, even when an enforcement path is still in flight.
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 propagation to existing repos runs on a platform job; the page text says ~5 minutes. New repos created after the toggle is on get the rule at creation.