Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
Auth policy

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:

  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 — choose the principal policy. Each SSH certificate carries one or more principals — short strings that name the user(s) it’s valid for:

PolicyMeaningWhen to use
UsernameCert principal = fremforge usernameDefault. Use when your CA emits certs with username as principal.
EmailCert principal = one of the user’s verified emailsUse when your IdP issues certs with email as principal.
AnythingCert principal can be any stringUse 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.

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 blockPOST /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 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.

ScopeSurfaces gated
web (default)Admin UI + REST API
web+gitAdmin 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:

  1. Configure the CIDR list on the Security page
  2. Set scope to web+git (the only value beyond web since 2026-05-22)
  3. Enable audit-only mode
  4. Wait 24-48 hours
  5. Review tenant_status_blocked and ip_allowlist_blocked events in the audit log
  6. Adjust CIDRs to cover any legitimate traffic that would have been refused
  7. 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.

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

PolicyWhere enforcement runs (today + planned)
Hardware-backed SSH keysAt Forgejo’s /-/user/settings/keys add-key form. Implemented in the SSH-key-add hook (Workstream C).
SSH CA — trusted CA keysStored 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 policyEnforced 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 authSame shim — when on, plain pubkey auth is refused before reaching git-receive-pack.
Max PAT lifetimeAt PAT mint time (PAT-mint wizard, Workstream C).
Allow repo deploy keysDefault 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 commitsPlatform job watches this column and writes branch-protection rules to Forgejo per repo (Workstream C).
IP allowlist scopetenantStatusMiddleware (/_app/middleware/tenant-status.ts) extended to read scope + cover the appropriate paths (Workstream A).
Audit-only modeSame 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.