
In short. A default Gitea install leaves the web installer open, registration unrestricted, and repositories public by default. This checklist covers the five `app.ini` settings that close those gaps (`INSTALL_LOCK`, `DISABLE_REGISTRATION`, `REQUIRE_SIGNIN_VIEW`, `RUN_USER`, secret keys), plus 2FA enforcement (Gitea 1.24+), access token scoping, private-by-default repository visibility, branch protection, and a hardened Nginx reverse-proxy configuration with the headers Gitea requires. OS and SSH hardening are covered in the linked guides below.
Before You Start: The VPS Security Baseline
Gitea hardening builds on top of a secured host. Before working through the steps below, make sure your VPS already has:
- SSH key authentication with password login disabled. See the SSH key authentication guide for setup instructions.
- The Contabo Firewall activated. Every Contabo VPS and VDS includes a free network-level firewall managed from the Customer Control Panel – no installation required. See our self-help article for a step-by-step walkthrough.
- Fail2Ban installed and configured. See the Fail2Ban guide to block repeated login attempts at the network level.
- A non-root sudo user running the service. See the sudo user guide for the correct way to provision a `git` system account.
- Regular host patching. See the VPS patching guide for an automated approach to keeping your OS packages current.
Each item is a one-sentence entry point; the linked tutorials cover the full procedure. Do not skip this baseline: Gitea-specific hardening does nothing for an attacker who already has SSH access through a weak host configuration.
Gitea-Specific Hardening (the Part Nothing Else Covers)
These items are unique to Gitea. They do not appear in any OS guide, and a default Gitea install does not apply most of them. Work through each section; all changes go into `app.ini`, which lives at `/etc/gitea/app.ini` on most Linux package installs or at `custom/conf/app.ini` in a binary install.
Lock Down the app.ini Configuration
Five settings in `app.ini` are unique to Gitea and not covered by any OS hardening guide. Set all of them before exposing your instance to the network:
`INSTALL_LOCK = true`closes the/installweb endpoint. The web installer sets this automatically on completion, but verify it is present inapp.inibefore going live — if it is missing orfalsefor any reason, anyone who reaches the endpoint can reconfigure your instance from scratch.- `REQUIRE_SIGNIN_VIEW = true` forces login before browsing any repository, issue, or user profile. Essential for private team instances.
- `RUN_USER = git` tells Gitea to run as a dedicated non-root system account, not as root. Check your systemd unit and fix this first if it is wrong.
- `SECRET_KEY` and `INTERNAL_TOKEN` are both auto-generated by the installer. Verify each is a long random string, not a blank or placeholder. Rotate them if you are unsure of their origin.
After any change to `app.ini`, restart and verify:
sudo systemctl restart gitea && sudo systemctl status giteaDisable Open Registration and Control Sign-Up
Set `DISABLE_REGISTRATION = true` in the `[service]` section of `app.ini`. This stops anyone from creating their own account; new users must be provisioned by an administrator via Site Administration > Users or the `gitea admin user create` CLI command. If you need external sign-in via OAuth or LDAP, you can still configure those sources for pre-existing accounts: create the user in Gitea first and then link the OAuth identity through their profile.
Enforce 2FA and Strong Auth
Gitea supports TOTP-based 2FA natively. Users enroll via Settings > Security > Two-Factor Authentication using any RFC 6238-compatible app (Google Authenticator, Authy, 1Password, and similar).
To enforce 2FA across all users, add the following to the `[service]` section of `app.ini` (requires Gitea 1.24 or later):
[service]
TWO_FACTOR_AUTH = enforcedAfter restarting, any user who has not enrolled in 2FA will be redirected to the enrollment page on their next login and locked out of repositories until they complete it. Gitea Enterprise has its own flag for this (`ENFORCE_TWO_FACTOR_AUTH` in `[security]`), but on community Gitea 1.24+, `TWO_FACTOR_AUTH = enforced` should cover the same ground.
Access Tokens and Deploy Keys Hygiene
Scope every personal access token to the minimum permissions it needs. Gitea supports fine-grained token scopes (for example, `read:repository` or `write:package`) under Settings > Applications. Set an expiry date on every token; ones that never expire accumulate silently. For read-only CI/CD access, create a dedicated token scoped to `read:repository` only rather than using a full-access personal token. Rotate any token on suspected exposure immediately.
Repository and Org Access Control
To make all new repositories private by default, set the following in the `[repository]` section of `app.ini`:
[repository]
DEFAULT_PRIVATE = trueThis means the “Private” checkbox is pre-ticked whenever a user creates a new repository. If you want to go further and prevent repositories from ever being made public, set `FORCE_PRIVATE = true` instead – administrators can still change visibility, but regular users cannot. For access control, use organisations and teams rather than direct per-user permissions, and assign the minimum role needed: Reader, Writer, or Owner. Enable branch protection on repositories that receive production deployments: go to Repository Settings > Branches, add a rule for your default branch, require at least one review approval, and disable force-push.
Reverse-Proxy and TLS Hardening for Gitea
Gitea can handle TLS directly, but the recommended approach is to put a reverse proxy in front and terminate TLS there. This section covers Nginx. Gitea does not need to know about TLS in this setup; it just needs to know its public URL.
app.ini – required settings for any reverse-proxy setup
Set these in the `[server]` and `[session]` sections before bringing the proxy up:
[server]
ROOT_URL = https://git.example.com/
[session]
COOKIE_SECURE = true`ROOT_URL` must match the public HTTPS URL exactly. A mismatch breaks clone URLs, OAuth redirects, and webhook payloads. `COOKIE_SECURE = true` marks session cookies as Secure so browsers only send them over HTTPS; without it, cookies issued through an HTTPS proxy can be replayed over HTTP.
Nginx server block
The proxy headers below are required by Gitea, not optional. Without `X-Forwarded-Proto` and `X-Real-IP`, Gitea cannot see the real client IP or determine the request scheme, which breaks IP-based rate limiting, audit logs, and OAuth flows.
server {
listen 443 ssl;
server_name git.example.com;
ssl_certificate /etc/letsencrypt/live/git.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy strict-origin-when-cross-origin;
# Hide Gitea version
proxy_hide_header X-Gitea-Version;
# Rate-limit the login and sign-up routes
limit_req zone=gitea_auth burst=5 nodelay;
location / {
client_max_body_size 512M;
proxy_pass http://127.0.0.1:3000;
# Required headers — do not remove
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_upgrade;
}
}
server {
listen 80;
server_name git.example.com;
return 301 https://$host$request_uri;
}Define the rate-limit zone in the `http` block of `nginx.conf` (outside the `server` block):
limit_req_zone $binary_remote_addr zone=gitea_auth:10m rate=10r/m;`client_max_body_size 512M` matches the value in the Gitea docs. Without it, repository pushes and file uploads over the default Nginx limit (1 MB) return a 413 error.
Issue TLS certificates via Let’s Encrypt with Certbot; see the How Do I Install an SSL Certificate on My Linux Server Using Let’s Encrypt? for the full procedure.
After any Nginx change, reload rather than restart to avoid dropping active connections:
sudo nginx -t && sudo systemctl reload nginxBackups and Update Discipline
Backing Up Gitea
Gitea ships a `dump` subcommand that archives repositories, the database, configuration, and attachments into a single zip file:
sudo systemctl stop gitea
sudo -u git gitea dump -c /etc/gitea/app.ini --file /backups/gitea-$(date +%Y%m%d).zip
sudo systemctl start giteaThe dump includes the SQLite database or a plain SQL export. For PostgreSQL or MySQL, take a separate database dump before running `gitea dump`:
# PostgreSQL
pg_dump -U gitea gitea > /backups/gitea-db-$(date +%Y%m%d).sql
# MySQL / MariaDB
mysqldump -u gitea -p gitea > /backups/gitea-db-$(date +%Y%m%d).sqlStore backups off-server. Contabo offers two complementary options:
- Auto Backup Add-On – a paid Add-On for Cloud VPS that creates daily automated backups of your entire VPS disk, stored off-server on a separate Contabo storage system for up to 10 days. This gives you a full system restore point, useful for rolling back a failed Gitea upgrade or recovering from a compromised host.
- Manual offload via rclone – the `gitea dump` archive is a portable, Gitea-specific export you control. Sync it to Contabo Object Storage with rclone for long-term retention beyond 10 days. See the rclone backup guide for the full procedure.
For a production Gitea instance, both are worth running: Auto Backup for fast full-system recovery, `gitea dump` plus Object Storage for long-term archival and portability.
Staying Current
Gitea releases patch versions frequently, and security fixes travel in patch releases. Subscribe to the Gitea release feed at `https://github.com/go-gitea/gitea/releases.atom` so you get notified. Before any update, take a full `gitea dump` and a database snapshot; Gitea’s database migrations run automatically on startup and are not reversible without a backup. If you run a team deployment, test updates on a staging instance first. Running a single patch version behind is acceptable; running multiple minor versions behind is not.
Detecting a Compromise
Gitea generates access logs, push logs, and admin audit logs. These are the first place to look if you suspect unauthorized access. See the indicators your instance has been compromised guide for a full list of signals to monitor and the response steps to take. As a baseline, watch for unexpected new user accounts, SSH keys added to existing accounts, and repositories that were private and are now public.
Why Run a Hardened Gitea on Contabo
Hosting Gitea on a Contabo VPS gives you ample resources. Your CI runners, your Git processes, and your database all get the CPU and RAM you paid for, without contention from other tenants.
For teams subject to GDPR or internal data-sovereignty policies, EU data residency matters. Choosing an EU data center keeps source code and commit history in EU jurisdiction, on infrastructure that is 100% GDPR-compliant.
Snapshots are worth highlighting in the context of Gitea updates. Before running a major upgrade, take a snapshot of the entire VPS through the Customer Control Panel. If the migration fails or introduces a regression, you roll back in minutes rather than restoring from a database backup.
Entry level VPS plans start at less than 7 bucks per month and give you enough headroom to run Gitea alongside a PostgreSQL database and an Nginx proxy without resource pressure.
FAQ: Gitea Security
Add `DISABLE_REGISTRATION = true` under `[service]` in `app.ini` and restart Gitea. New accounts must then be created by an administrator via Site Administration > Users or `gitea admin user create`. Note that this also blocks OAuth-based account creation – create users manually first and link OAuth identities through their profile.
Yes. Users enroll via Settings > Security > Two-Factor Authentication using any TOTP app. To enforce it site-wide, add `TWO_FACTOR_AUTH = enforced` under `[service]` in `app.ini` and restart (requires Gitea 1.24+). Gitea Enterprise has a separate flag for this. API access via personal tokens is not gated by 2FA regardless of edition.
Set ROOT_URL = https://git.example.com/ in [server] and COOKIE_SECURE = true in [session] in app.ini. Point Nginx at http://127.0.0.1:3000 and pass the required headers (Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto). Issue a certificate via Certbot – see How Do I Install an SSL Certificate on My Linux Server Using Let’s Encrypt? for the full procedure.
Run gitea dump -c /etc/gitea/app.ini to produce a zip archive of repositories, configuration, database, and attachments. For PostgreSQL or MySQL, take a separate pg_dump or mysqldump first. Store archives off-server – Contabo Auto Backup covers full-disk daily snapshots, or sync the dump to Object Storage with rclone for long-term retention.