Developer Tools AWS

Gitea 28.2.0 on AWS User Guide

| Product: Gitea 28.2.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Gitea 28.2.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Apache Subversion (SVN) and CI/CD Tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Gitea is a lightweight, self hosted Git service written in Go. It gives a team Git hosting with a familiar web interface: repositories with branch protection and code review through pull requests, issues and milestones, a wiki per repository, releases, a package registry, organisations and teams, and Gitea Actions for CI. This image runs the official Gitea 28.2.0 release with a SQLite database on the instance, so there is no separate database server to run.

The cloudimg image adds:

  • HTTPS from the first boot. nginx serves Gitea on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • An administrator and secrets unique to your instance. First boot creates the database and the administrator gitea_admin with a password generated on your instance, and generates Gitea's secret key, internal token and token signing secrets. Nothing secret is baked into the image; the settings file holds no secret at all.
  • Private by default. Sign in is required to see anything, visitors cannot register their own accounts (the administrator creates them), and Gitea's web installer is locked.
  • Fail closed. Gitea refuses to start until first boot has finished, and so does nginx.
  • Gitea on loopback only. Gitea's web server listens on 127.0.0.1:3000 behind nginx. Git over SSH uses Gitea's built in SSH server on port 2222 (key authentication only, no shell), so port 22 stays your administrator SSH. The open ports are 22, 80, 443 and 2222.
  • Protection against server side request forgery. Webhooks, repository migrations and mirrors cannot reach private, loopback or link local addresses such as the instance metadata service, and Gitea and nginx are blocked from the metadata service at the kernel level. The AMI requires IMDSv2.
  • Hardened defaults. Gitea runs as its own unprivileged git user in a systemd sandbox that can write only /var/lib/gitea. Its public address comes from your configuration, never from a request, and requests for a host name the instance does not serve are dropped.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Gitea 28.2.0 (the official gitea-28.2.0-linux-amd64 release binary, checksum verified), git, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (administrator SSH), TCP 443 (Gitea), TCP 80 (redirects to HTTPS) and TCP 2222 (git over SSH) from your own networks only.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Gitea 28.2.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. Repositories and the database live under /var/lib/gitea on the root volume; size the root volume for the repositories you expect to host.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=gitea}]'

Step 2: Connect to your instance

Version SSH user
Gitea 28.2.0 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot takes a few seconds. Then the three units are active:

systemctl is-active gitea-firstboot gitea nginx
active
active
active

Gitea's web server listens on loopback only, its SSH server on 2222, and nginx serves ports 80 and 443. (The second loopback port is Gitea's internal proxy for outbound git operations.)

sudo ss -tlnp | awk 'NR==1 || /gitea|nginx|sshd/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 127.0.0.1:44181 users:(("gitea"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 127.0.0.1:3000 users:(("gitea"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN *:2222 users:(("gitea"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))

The version on the image:

gitea --version
gitea version 28.2.0 built with go1.27.2

Step 4: Get your URL and password

sudo cat /root/gitea-credentials.txt

The file holds the address Gitea is served as (your instance's public DNS name by default), the administrator user and the password generated on this instance, and the form of an SSH clone URL:

GITEA_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
GITEA_ADMIN_USER=gitea_admin
GITEA_ADMIN_PASSWORD=<24 letters and digits>
GITEA_SSH_PORT=2222
GITEA_SSH_CLONE=ssh://git@ec2-<public-ip-dashed>.<region>.compute.amazonaws.com:2222/<owner>/<repo>.git

Step 5: Sign in from your browser

Open GITEA_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or install your own certificate (Step 9). Sign in with gitea_admin and the password from Step 4.

The Gitea sign in form with the gitea_admin user name and a masked password filled in

The sign in page has no Register link: on this image only the administrator can create accounts. After signing in you see your dashboard, with your activity and your repositories:

The Gitea dashboard for gitea_admin showing the contribution graph, a recent activity entry and the repository list

To change the administrator password, open the user menu at the top right, Settings, Account.

Step 6: Create a repository and push to it

In the browser, choose + at the top right, New Repository, give it a name and choose Create Repository. The repository page shows the clone URLs under Code, for HTTPS and for SSH (port 2222):

A new Gitea repository page with its README and the Code menu open showing the HTTPS clone URL and the SSH and Tea CLI tabs

From your workstation, push an existing project over HTTPS (git asks for your Gitea user name and password; while the certificate is self signed, tell git to accept it for this host only):

git -c http.sslVerify=false push https://<gitea-host>/<owner>/<repo>.git main

Or add your SSH public key in the browser (user menu, Settings, SSH / GPG Keys, Add Key) and use the SSH URL on port 2222:

git clone ssh://git@<gitea-host>:2222/<owner>/<repo>.git

You can try both from the instance itself. This creates a private repository called demo through Gitea's API; the password is handed to curl in a file, so it never appears on a command line:

H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
CFG=$(mktemp)
sudo sed -n 's/^GITEA_ADMIN_PASSWORD=\(.*\)/user = "gitea_admin:\1"/p' /root/gitea-credentials.txt > "$CFG"
curl -sk --resolve "$H:443:127.0.0.1" -K "$CFG" -H 'Content-Type: application/json' -o /dev/null -w 'create repository: %{http_code}\n' \
  -X POST "https://$H/api/v1/user/repos" -d '{"name":"demo","private":true}'
rm -f "$CFG"
create repository: 201

Push a first commit to it over HTTPS (git reads the password through a small helper script, again never from a command line):

H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
W=$(mktemp -d); sudo sed -n 's/^GITEA_ADMIN_PASSWORD=//p' /root/gitea-credentials.txt > "$W/pw"
printf '#!/bin/sh\ncase "$1" in Username*) echo gitea_admin ;; *) cat "%s/pw" ;; esac\n' "$W" > "$W/askpass"; chmod 700 "$W/askpass"
git -C "$W" init -q -b main demo && echo "# demo" > "$W/demo/README.md"
git -C "$W/demo" add README.md && git -C "$W/demo" -c user.name=Admin -c user.email=admin@example.com commit -qm "first commit"
GIT_ASKPASS="$W/askpass" git -C "$W/demo" -c http.sslVerify=false -c "http.curloptResolve=$H:443:127.0.0.1" push -q "https://$H/gitea_admin/demo.git" main && echo "pushed over HTTPS"
rm -rf "$W"
pushed over HTTPS

Then add an SSH key for gitea_admin and clone the repository back over SSH on port 2222:

H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
W=$(mktemp -d); ssh-keygen -q -t ed25519 -N '' -C demo -f "$W/id"
sudo sed -n 's/^GITEA_ADMIN_PASSWORD=\(.*\)/user = "gitea_admin:\1"/p' /root/gitea-credentials.txt > "$W/cfg"
curl -sk --resolve "$H:443:127.0.0.1" -K "$W/cfg" -H 'Content-Type: application/json' -o /dev/null -w 'add SSH key: %{http_code}\n' \
  -X POST "https://$H/api/v1/user/keys" -d "{\"title\":\"demo\",\"key\":\"$(cat "$W/id.pub")\"}"
GIT_SSH_COMMAND="ssh -i $W/id -o HostName=127.0.0.1 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$W/known_hosts" \
  git clone -q "ssh://git@$H:2222/gitea_admin/demo.git" "$W/demo" && echo "cloned over SSH: $(cat "$W/demo/README.md")"
rm -rf "$W"
add SSH key: 201
cloned over SSH: # demo

The first time you connect over SSH from your workstation, compare the host key fingerprint with the one your instance generated:

sudo ssh-keygen -lf /var/lib/gitea/data/ssh/gitea.ed25519.pub
256 SHA256:<fingerprint of your instance> no comment (ED25519)

Step 7: Users, organisations and administration

As gitea_admin, open the user menu and Site Administration. The dashboard shows maintenance operations and system status; Identity & Access, User Accounts, Create User Account adds a user (they can change the password you give them at first sign in). Organisations and teams are created from +, New Organization.

The Gitea Site Administration dashboard listing maintenance operations and the system status

To let people register their own accounts instead, set DISABLE_REGISTRATION = false (and SHOW_REGISTRATION_BUTTON = true) in the [service] section of /etc/gitea/app.ini. To let visitors browse public repositories without signing in, set REQUIRE_SIGNIN_VIEW = false. Edit the file with sudo nano /etc/gitea/app.ini, then restart:

sudo systemctl restart gitea

Step 8: Security checks

Port 80 sends browsers to HTTPS, nothing is visible without signing in, a visitor cannot register, and nginx closes the connection for a host name the instance does not serve (000 means no answer):

H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'sign in page: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/user/login"
curl -sk -o /dev/null -w 'explore without signing in: %{http_code} -> %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/explore/repos"
curl -sk -o /dev/null -w 'self registration: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST "https://$H/user/sign_up" \
  -d 'user_name=visitor&email=visitor%40example.com&password=Visitor12345x&retype=Visitor12345x'
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
sign in page: 200
explore without signing in: 303 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/user/login?redirect_to=%2Fexplore%2Frepos
self registration: 403
unknown host name: 000

Gitea refuses to fetch from private and metadata addresses. A migration that asks it to import from the instance metadata service is refused:

H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
CFG=$(mktemp)
sudo sed -n 's/^GITEA_ADMIN_PASSWORD=\(.*\)/user = "gitea_admin:\1"/p' /root/gitea-credentials.txt > "$CFG"
curl -sk --resolve "$H:443:127.0.0.1" -K "$CFG" -H 'Content-Type: application/json' -w ' (HTTP %{http_code})\n' \
  -X POST "https://$H/api/v1/repos/migrate" -d '{"clone_addr":"http://169.254.169.254/latest/meta-data/","repo_name":"metadata-test","service":"git"}'
rm -f "$CFG"
{"message":"You can not import from disallowed hosts.","url":"https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/api/swagger"} (HTTP 422)

The settings behind this: Gitea listens on loopback, builds its links from its configured address only, trusts forwarded client addresses from nginx on 127.0.0.1 only, keeps the installer locked, blocks outbound requests to non public addresses (EGRESS_MODE = lax with no allow list) and keeps registration off with sign in required:

sudo grep -E '^(HTTP_ADDR|PUBLIC_URL_DETECTION|SSH_PORT|INSTALL_LOCK|REVERSE_PROXY_TRUSTED_PROXIES|EGRESS_MODE|BLOCKED_HOST_LIST|DISABLE_REGISTRATION|REQUIRE_SIGNIN_VIEW) ' /etc/gitea/app.ini
HTTP_ADDR = 127.0.0.1
PUBLIC_URL_DETECTION = never
SSH_PORT = 2222
INSTALL_LOCK = true
REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.1/32
EGRESS_MODE = lax
EGRESS_MODE = lax
BLOCKED_HOST_LIST = 169.254.0.0/16
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true

The secrets were generated on this instance and live in their own files, readable by root and the git user only:

sudo ls -l /etc/gitea/secrets | awk 'NR>1 {print $1, $3, $4, $NF}'
-rw-r----- root git internal_token
-rw-r----- root git jwt_secret
-rw-r----- root git lfs_jwt_secret
-rw-r----- root git secret_key

Gitea runs unprivileged in a sandbox, and neither Gitea nor nginx can reach the instance metadata service:

systemctl show gitea -p User -p ProtectSystem -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
User=git
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

The certificate was generated on this instance and names its own addresses:

H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
    DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

Step 9: Your own DNS name and certificate

Point a DNS record at the instance, then set the name Gitea is served as. This mints a new self signed certificate for the name, updates nginx, Gitea's DOMAIN, ROOT_URL and SSH_DOMAIN (so clone URLs use it) and the URL in /root/gitea-credentials.txt, restarts Gitea, and keeps the name across reboots:

sudo gitea-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running gitea-set-hostname again replaces it with a self signed one):

sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/gitea.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/gitea.key
sudo nginx -t && sudo systemctl reload nginx

Step 10: Your data, backups and upgrades

Repositories, LFS objects, attachments, packages and the SQLite database live under /var/lib/gitea/data:

sudo ls /var/lib/gitea/data
actions_artifacts
actions_log
attachments
avatars
gitea-repositories
gitea.db
home
indexers
jwt
lfs
packages
queues
repo-archive
repo-avatars
sessions
ssh
tmp

Back up /var/lib/gitea and /etc/gitea (the settings and the secrets files) together, or snapshot the root volume. Gitea's own gitea dump command writes a single archive of the database, repositories and settings; run it as the git user and keep a copy of /etc/gitea/secrets with it:

sudo -u git /usr/local/bin/gitea dump --config /etc/gitea/app.ini --file /var/lib/gitea/gitea-dump.zip

Ubuntu's unattended upgrades keep the operating system, git and nginx patched. To move to a newer Gitea release later, snapshot the instance, download the new gitea-<version>-linux-amd64 binary from the Gitea releases page, check it against the published .sha256, replace /usr/local/bin/gitea and run sudo systemctl restart gitea (Gitea migrates the database on start). Read the release notes first for breaking changes.

Step 11: Server components

Component Path
Gitea binary /usr/local/bin/gitea
Settings /etc/gitea/app.ini
Secrets (generated on first boot) /etc/gitea/secrets/
Repositories /var/lib/gitea/data/gitea-repositories
Database (SQLite) /var/lib/gitea/data/gitea.db
LFS objects /var/lib/gitea/data/lfs
Gitea SSH host keys /var/lib/gitea/data/ssh/
Logs journalctl -u gitea
Service units /etc/systemd/system/gitea.service, /etc/systemd/system/gitea-firstboot.service
nginx site /etc/nginx/sites-available/cloudimg-gitea
Certificate and key /etc/nginx/ssl/
Host name helper /usr/local/sbin/gitea-set-hostname
Credentials /root/gitea-credentials.txt (mode 0600)

Step 12: Security recommendations

  • Allow ports 22, 80, 443 and 2222 in the security group from your own networks only.
  • Keep registration off and sign in required unless you need them, and create accounts for your team as the administrator.
  • Keep the administrator password secret and change it if it may have been shared (Step 5); consider two factor authentication (user menu, Settings, Security).
  • Leave the outbound request settings as shipped. If a webhook or mirror must reach a host on your private network, allow that one address in [security] ALLOWED_HOST_LIST (and [migrations] ALLOWED_HOST_LIST for migrations), never 169.254.0.0/16.
  • Use your own DNS name and a certificate your browsers and git clients trust (Step 9).
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.
  • Back up /var/lib/gitea and /etc/gitea regularly (Step 10).

Support and licensing

Gitea is open source under the MIT licence (the licence text is at /usr/share/doc/gitea/LICENSE); there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA