A self-hosted Git server holds source code, deployment keys, CI secrets and the power to change what ships, yet many Forgejo and Gitea instances run with open registration, password logins over HTTPS, broadly scoped tokens and Actions runners that execute untrusted workflows on the host. Forgejo and Gitea are lightweight Git forges with the same roots: Forgejo began as a fork of Gitea, and both share most configuration keys in app.ini, the same token scope families and a GitHub Actions compatible CI system with separate runners. Platforms are listed alphabetically on this page; order is not a ranking. This guide covers authentication and two-factor enforcement, site administrators and Git hooks, personal access tokens, OAuth2 applications and runner registration tokens, SSH, logging and a hardening baseline with real setting names and defaults. It is for platform engineers and security teams running either forge, using Forgejo's documentation and Gitea's v1.27.3 configuration reference and runner documentation as checked in November 2026.
This is an independent guide from NHI Mgmt Group, based on the Forgejo and Gitea projects' public documentation as of November 2026. It is not sponsored or reviewed by the Forgejo or Gitea projects. Product features change, so confirm settings against the Forgejo and Gitea projects' current documentation.
Key takeaways
- Close the front door first. In both forges
DISABLE_REGISTRATIONdefaults to false andREQUIRE_SIGNIN_VIEWto false: an internet-facing instance lets anyone create an account and browse public content. Turn registration off or route it through SSO. - Enforce two-factor authentication. Forgejo uses
GLOBAL_TWO_FACTOR_REQUIREMENT(none,adminorall); Gitea usesTWO_FACTOR_AUTH = enforced. Both default to no requirement. - Git hooks are remote code execution. Custom server-side hooks run as the forge's operating system user and can be used to become site administrator. Keep
DISABLE_GIT_HOOKS = trueand patch promptly. - Runners are the biggest blast radius. A
hostlabel runs jobs on the machine with no isolation, and jobs there can read the runner's credentials. Use container labels, ephemeral registration and the narrowest registration scope. - Tokens need scopes and a place to live. Every token needs at least one scope; create read-only, narrowly scoped tokens for automation, and set
DISABLE_QUERY_AUTH_TOKEN = trueso tokens never appear in URLs and logs.
How identity and access work in Forgejo and Gitea
Both forges keep local user accounts and can add external authentication sources: LDAP, SMTP, PAM, OAuth2 and OpenID Connect providers, and reverse proxy authentication. Access to repositories comes from ownership, organisation teams and collaborators. Git operations authenticate over SSH with user keys or over HTTPS with a password or token.
| Setting | Section | Default | Hardened value |
|---|---|---|---|
DISABLE_REGISTRATION | [service] | false | true, or ALLOW_ONLY_EXTERNAL_REGISTRATION through SSO |
REQUIRE_SIGNIN_VIEW | [service] | false | true for internal instances |
ENABLE_BASIC_AUTHENTICATION | [service] | true | false once users rely on tokens and SSH; it disables password basic auth but not token basic auth |
ENABLE_CAPTCHA | [service] | false | true if registration must stay open |
MIN_PASSWORD_LENGTH | [security] | 8 | 12 or more |
PASSWORD_CHECK_PWN | [security] | false | true, if outbound calls to the breached password service are acceptable |
PASSWORD_HASH_ALGO | [security] | pbkdf2 | argon2 or a tuned pbkdf2, applied as users change passwords |
REVERSE_PROXY_TRUSTED_PROXIES | [security] | 127.0.0.0/8,::1/128 | Only your proxy addresses; never * |
- Two-factor enforcement: Forgejo's
GLOBAL_TWO_FACTOR_REQUIREMENTacceptsnone(default),adminorall. Gitea'sTWO_FACTOR_AUTH = enforcedforces enrolment, and users without it lose web and API access to repositories. Gitea also exposesENABLE_PASSKEY_AUTHENTICATIONand can hide the password form withENABLE_PASSWORD_SIGNIN_FORM = false. See the Passkey Rollout Guide. - Reverse proxy authentication: when
ENABLE_REVERSE_PROXY_AUTHENTICATIONis on, the forge trusts theX-WEBAUTH-USERheader. Gitea's reference warns the forge must only be reachable through the proxy, which must set or strip the header on every request. A forge reachable directly on its port with this enabled lets anyone claim any identity. - HTTPS passwords are a separate door: the PHP Git Server Compromise 2021 came through password logins over HTTPS that bypassed the key-based SSH access layer. Disable basic password authentication and require tokens or SSH.
Admin roles and privileged access
Site administrators can change any repository, user and authentication source, and reach the configuration and monitoring screens. Organisation owners and repository administrators hold more limited but still powerful rights, including adding collaborators, managing deploy keys and secrets, and changing visibility.
- Git hooks:
DISABLE_GIT_HOOKSdefaults to true in both forges. Gitea's configuration reference explains why: custom hooks allow arbitrary code execution on the host as the forge user, access toapp.iniand the database, and from there site administrator rights. Setting it to true does not remove hooks already created, so audit repositories if it was ever false. Never grant the "may create Git hooks" permission to ordinary users. - Patch the forge itself: apply security updates promptly and treat repository write access as a privileged right on any instance that is behind on updates. Forgejo publishes advisories in the
forgejo/security-announcementsrepository on Codeberg; Gitea publishes on its blog and in its GitHub security advisories. Subscribe to whichever you run. - Local path imports: keep
IMPORT_LOCAL_PATHS = falseso users cannot import directories from the server's file system as repositories. - Limit creation: on shared instances, set
MAX_CREATION_LIMIT(default-1, unlimited) andDISABLE_REGULAR_ORG_CREATIONto stop spam and resource abuse. Forgejo's runner security guidance also recommendsALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = false, since forking otherwise bypasses the repository limit. - Few, named administrators: keep the site administrator group to two or three named people with two-factor or passkeys, separate from their daily accounts. Default or shared admin logins are a known route in: the Nissan Source Code Leak 2021 followed an internet-facing Git server left on its factory administrator login.
- Installer lock: make sure
INSTALL_LOCK = trueafter setup so the web installer cannot be rerun.
Integrations, API tokens and service accounts
Machine identities on a forge include personal access tokens used by scripts and integrations, OAuth2 applications, deploy keys, webhooks with shared secrets, Actions secrets and runner registrations. Each one can read or change code. See the Secrets Management Guide for storage and rotation.
| Machine identity | What it can do | Controls |
|---|---|---|
| Personal access token | Act as its user within its scopes: activitypub, admin, issue, misc, notification, organization, package, repository, user, each read: or write: | At least one scope is required. Use read scopes where possible; never give automation write:admin. Create tokens under dedicated bot accounts, not personal ones |
| Deploy key | SSH read, or read and write, on one repository | Read-only unless the job pushes; one key per consumer; remove when the consumer is retired |
| OAuth2 application | The forge is an OAuth2 provider ([oauth2] ENABLED = true, RS256 signing by default) | Review registered applications and granted authorisations; disable the provider if unused |
| Webhook | Sends events to other systems | [webhook] ALLOWED_HOST_LIST defaults to external, which blocks private network targets; keep it, and use a secret on every hook |
| Runner registration token | Registers runners that receive jobs and their secrets | Narrowest scope; one token can register any number of runners until reset, so reset it after use |
| Actions secrets | Available to workflows at runtime | Fork pull_request workflows get none; pull_request_target workflows get them and need careful review |
Availability: Forgejo's token page adds resource restrictions to scopes: All, Public only, or Specific repositories, where only repository and issue scopes apply and repository administration is blocked. Gitea's token scopes are defined in its access token model with the same families listed above; check the token creation screen of your Gitea release for its current repository and organisation access options.
- Keep tokens out of URLs: both forges default
DISABLE_QUERY_AUTH_TOKENto false, which still accepts tokens in query strings. Set it to true so tokens arrive only in theAuthorizationheader and never land in proxy logs or browser history. API tokens are stored as pbkdf2 hashes. - Protect the instance secrets:
SECRET_KEYencrypts data such as two-factor secrets (losing it makes them undecryptable) andINTERNAL_TOKENauthenticates internal calls. Load them from files withSECRET_KEY_URIandINTERNAL_TOKEN_URIsoapp.inican be managed in configuration tooling without holding secrets.
Actions runners
Runners pull jobs from the forge and execute them. Their isolation decides whether a malicious workflow, or a workflow fed malicious input, stays inside a throwaway container or reaches the host, the network and other jobs' secrets.
| Runner choice | Forgejo | Gitea | Recommendation |
|---|---|---|---|
| Execution labels | docker, lxc, host; host runs without isolation and lxc without resource limits | docker://, kubernetes://, host; host jobs run as the runner user without isolation, for trusted repositories only | Container or pod labels for anything that builds untrusted code |
| Registration scope | Repository, organisation or instance (global runners take jobs from any repository) | Instance, organisation, user or repository | Narrowest scope that works; separate runners for sensitive repositories |
| Ephemeral mode | --ephemeral, enforced by Forgejo, used with forgejo-runner one-job | --ephemeral or GITEA_RUNNER_EPHEMERAL=1; credentials revoked once the job is assigned (daemon --once leaves them valid) | Ephemeral runners for untrusted or public repositories |
| Runner credential file | .runner readable by jobs in host mode; theft lets an attacker impersonate the runner and receive secrets | .runner (or runner.file) must be treated as a secret; one per runner process | Restrict permissions; re-register if exposed |
| Container settings | container.privileged = false, container.valid_volumes empty, container.network = "" for a per-job network, limits via container.options and runner.timeout | Registration token passed with --token-file or GITEA_RUNNER_REGISTRATION_TOKEN to keep it out of shell history | Apply both columns' practices whichever forge you run |
- Pin actions: Gitea's
DEFAULT_ACTIONS_URLdefaults togithub, souses:references resolve to github.com. Forgejo recommends pinning remote actions to a commit SHA and reviewing changes. Consider mirroring approved actions locally. See the CI/CD Security Guide for pipeline controls that carry over. - Fork pull requests: in Forgejo the first pull request from a fork needs approval before workflows run, but later ones from the same fork do not. Anyone with push access to a new branch can trigger
on: pushworkflows. - Network placement: isolate runner subnets from internal systems with firewall rules or VLANs, and do not log runner hosts into private registries unless workflows are meant to pull from them.
SSH and network exposure
- Built-in or system SSH:
START_SSH_SERVERdefaults to false, meaning the forge relies on the host's OpenSSH with forced commands. If you enable the built-in server, setSSH_SERVER_CIPHERS,SSH_SERVER_KEY_EXCHANGESandSSH_SERVER_MACSto modern values and keepMINIMUM_KEY_SIZE_CHECK = trueso weak user keys are rejected. See the OpenSSH Server Hardening Guide. - Do not expose it if you do not need to: an internal forge belongs behind a VPN or identity-aware proxy. If it must be public, publish only HTTPS through a reverse proxy and SSH, and keep the application port bound to localhost.
- TLS and cookies: set
ROOT_URLto an HTTPS address so session cookies are marked secure (Gitea'sCOOKIE_SECUREdefaults to true whenROOT_URLis HTTPS). - Outbound calls: webhooks, migrations and mirrors make the server fetch URLs. Keep the webhook allowlist at
externaland restrict migration hosts so the forge cannot be used for server-side request forgery against internal services.
Logging and detection
Both forges log to the console by default ([log] MODE = console, LEVEL = Info). Enable the access logger (logger.access.MODE) and, for SSH, ENABLE_SSH_LOG (default false), then ship logs to central storage.
| Signal | Why it matters | ATT&CK |
|---|---|---|
| New site administrator or new authentication source | Persistence and identity takeover | T1098 Account Manipulation |
| Bursts of failed logins or token use from new IPs | Password spraying, stolen tokens | T1110 Brute Force, T1078 Valid Accounts |
| Mass clone or archive downloads by one account | Source code theft | T1213.003 Code Repositories |
| New deploy keys or SSH keys on privileged accounts | Durable access outside SSO | T1098 |
New runner registrations, especially host labels | A rogue runner receives jobs and secrets | T1528 Steal Application Access Token |
| Hook files appearing in repository directories on disk | Code execution on the server | T1190 Exploit Public-Facing Application |
Measures that show progress: share of accounts with two-factor or passkeys (target 100% for administrators and organisation owners), number of tokens with write or admin scopes and their age, share of runners that are ephemeral and container-based, days from a forge advisory to patch (target under 3 for critical issues), and the number of instances reachable from the internet without SSO in front.
Hardening baseline
- Inventory: list every Forgejo and Gitea instance, version, exposure, authentication sources, administrators and runners. Retire unmaintained instances.
- Patch: move to the current supported release and subscribe to the project's security announcements.
- Lock registration and sign-in:
DISABLE_REGISTRATION = true,REQUIRE_SIGNIN_VIEW = truefor internal forges, SSO through OIDC, two-factor enforced for all users. - Secrets out of
app.ini: useSECRET_KEY_URIandINTERNAL_TOKEN_URI; back up the secret key with the database, since losing it breaks two-factor. - Tokens: set
DISABLE_QUERY_AUTH_TOKEN = true, disable password basic authentication once integrations move to tokens, and review all tokens with write or admin scopes. - Runners: replace
hostrunners with container runners, register them ephemeral at repository or organisation scope, and reset registration tokens. - Network: reverse proxy with TLS, application port on localhost, VPN or identity-aware proxy for internal use, firewall rules on runner subnets.
- Test and roll back: apply changes on a staging copy restored from backup, keep the previous
app.iniand binary or image, and announce the date password Git access stops so users can switch to tokens or SSH first. See the Git Repository Security Guide.
Common misconfigurations
- Open registration on a public instance: spam accounts, abuse of storage and Actions, and a source of write access you did not intend to grant.
- A
hostrunner shared by many repositories: any workflow can read the runner token and.runnerfile and reach the host network. - Reverse proxy authentication with a reachable backend: anyone who can reach the application port can set
X-WEBAUTH-USER. - Trusting all proxies:
REVERSE_PROXY_TRUSTED_PROXIES = *lets clients forge their source address. - Admin-scoped tokens in CI: a leaked pipeline variable becomes full control of the forge.
- Webhook allowlist widened to
*: turns the forge into a request forgery tool against internal services and cloud metadata.
Framework mapping
| Area | NIST SP 800-53 Rev. 5 | MITRE ATT&CK |
|---|---|---|
| Registration, sign-in and two-factor | IA-2, IA-5 | T1078 Valid Accounts, T1110 Brute Force |
| Administrators and Git hooks | AC-6, CM-7 | T1098 Account Manipulation |
| Tokens, deploy keys and runner credentials | IA-5, SC-12 | T1528 Steal Application Access Token, T1552.004 Private Keys |
| Patching the forge | SI-2, RA-5 | T1190 Exploit Public-Facing Application |
| Runners and actions | SR-3, CM-7 | T1195.002 Compromise Software Supply Chain |
| Logging and repository access monitoring | AU-2, AU-6 | T1213.003 Code Repositories |
Practitioner checklist
- Patch to the current release and subscribe to Forgejo or Gitea security announcements.
- Set
DISABLE_REGISTRATION = trueandINSTALL_LOCK = true, andREQUIRE_SIGNIN_VIEW = trueon internal instances. - Enforce two-factor for all users with
GLOBAL_TWO_FACTOR_REQUIREMENT = all(Forgejo) orTWO_FACTOR_AUTH = enforced(Gitea). - Confirm
DISABLE_GIT_HOOKS = trueand audit repositories for existing custom hooks. - Set
DISABLE_QUERY_AUTH_TOKEN = trueand review every token with write or admin scope. - Move
SECRET_KEYandINTERNAL_TOKENto files referenced by URI and back them up securely. - Replace
hostrunners with ephemeral container runners at the narrowest registration scope. - Keep the webhook allowlist at
externaland pin actions to commit SHAs. - Enable access and SSH logging and alert on new admins, keys, runners and mass clones.
Standards and references
- Forgejo: Configuration Cheat Sheet
- Forgejo: Actions Security
- Forgejo: Access Token Scope
- Gitea: app.example.ini Configuration Reference (v1.27.3)
- Gitea: Runner Registration
- Gitea: Act Runner
- MITRE ATT&CK: Code Repositories
Related NHI Mgmt Group resources: Git Repository Security Guide · CI/CD Security Guide · OpenSSH Server Hardening Guide · Secrets Management Guide