Install a Gitea server

Wishing to detach myself from GAFAM, I have left GitHub without looking for an equivalent for a long time. Recently, the desire to display my repositories in a sexy way (and other) has risen to a breathtaking speed in my personal TODO. I had already heard about cgit, gitweb… So I tried those solutions and the sexy side was missing (although they’re very interesting). I told my browser to look for the “best alternative github”, the first result was Gitea.

Gitea is a fork of Gogs (the great painless self-hosted Git service) and it looks very nice! In this article, we are going to see how to deploy a self-hosted Gitea server, as usual, with the best security practices!

Information and requirements

These elements are to be taken into consideration to follow this article:

Update the system

[operator@gitea ~]$ sudo dnf -y update

Install required packages

[operator@gitea ~]$ sudo dnf -y install git epel-release httpd mod_ssl vim firewalld policycoreutils-python-utils unzip

Install PostgreSQL

Install the RPM repository

[operator@gitea ~]$ sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm

Disable the built-in PostgreSQL module

[operator@gitea ~]$ sudo dnf -y module disable postgresql

Install PostgreSQL

[operator@gitea ~]$ sudo dnf -y install postgresql14-server

Initialize database

[operator@gitea ~]$ sudo /usr/pgsql-14/bin/postgresql-14-setup initdb
Initializing database ... OK

Enable and start PostgreSQL service

[operator@gitea ~]$ sudo systemctl enable --now postgresql-14

Configure database

Create role

Choose a strong password for role gitea. You can generate a strong password (here 40 characters but you can increase its size) with the following statement.

Note: /dev/urandom and /dev/random are using the exact same CSPRNG (Cryptographically Secure PseudoRandom Number Generator).

[operator@gitea ~]$ tr -cd "[:alnum:]" < /dev/urandom | fold -w 39 | head -n 1
[operator@gitea ~]$ sudo -iu postgres
[postgres@gitea ~]$ createuser -sdRP gitea
Enter password for new role:
Enter it again:

Create the database

[postgres@gitea ~]$ createdb -E UTF8 -O gitea --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8' -T template0 giteadb
[postgres@gitea ~]$ exit
logout

Test the connection

[operator@gitea ~]$ psql -h localhost -U gitea giteadb
Password for user gitea:

If you see the following text, you have succeeded!

psql (14.5)
Type "help" for help.

giteadb=#

Install and configrure Gitea

Download the binary

Check the latest release of the file downloaded via cURL here.

[operator@gitea ~]$ curl -LOsSf https://dl.gitea.io/gitea/1.17.0/gitea-1.17.0-linux-amd64
[operator@gitea ~]$ sudo mv gitea-1.17.0-linux-amd64 /usr/local/bin/gitea
[operator@gitea ~]$ sudo chmod +x /usr/local/bin/gitea

Create user to run Gitea server

[operator@gitea ~]$ sudo adduser -r -s /bin/bash -c "Gitea server" -U -m git

Create the directory structure

[operator@gitea ~]$ sudo mkdir -p /var/lib/gitea/{custom,data,log}
[operator@gitea ~]$ sudo chown -R git: /var/lib/gitea/
[operator@gitea ~]$ sudo chmod -R 750 /var/lib/gitea/
[operator@gitea ~]$ sudo mkdir /etc/gitea
[operator@gitea ~]$ sudo chown root:git /etc/gitea
[operator@gitea ~]$ sudo chmod 770 /etc/gitea

Create the service file

Copy the following snippet into /etc/systemd/system/gitea.service.

[Unit]
Description=Gitea server
After=network.target
After=mariadb.service

[Service]
LimitMEMLOCK=infinity
LimitNOFILE=65535
LimitNPROC=64
RestartSec=2s
Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
RuntimeDirectory=gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
PrivateTmp=true
PrivateDevices=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Restart systemctl daemon

[operator@gitea ~]$ sudo systemctl daemon-reload

Enable and start Gitea service

[operator@gitea ~]$ sudo systemctl enable --now gitea

SELinux stuff

If SELinux is enabled Gitea will not start. You must change the SELinux content of the binary.

[operator@gitea ~]$ sudo restorecon -v /usr/local/bin/gitea

You’re still not quite done with SELinux, you must allow HTTPD scripts and modules to connect to the network.

[operator@gitea ~]$ sudo setsebool -P httpd_can_network_connect on

The -P flag allows to make this modification persistent even after a reboot.

Reverse proxy configurations

General TLS configurations

By adding ServerTokens Prod directive, you remove the Apache2 version from HTTP headers, it’s a good practice for security reasons.

[operator@gitea ~]$ sudo sed -i "352i ServerTokens Prod" /etc/httpd/conf/httpd.conf

Add these headers and the cache configuration for OCSP stapling just after #SSLCryptoDevice ubsec into /etc/httpd/conf.d/ssl.conf. Also, set the Diffie-Hellman parameters’ path and the temporary curve used for ephemeral ECDH modes.

# Headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options DENY
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy no-referrer

# OCSP stapling
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

# Key exchange
SSLOpenSSLConfCmd DHParameters "/etc/pki/tls/private/dhparam.pem"
SSLOpenSSLConfCmd Curves secp384r1

Next, you must generate the Diffie-Hellman’s parameters. Here, the size of the generated parameters set will be 4096 bits.

[operator@gitea ~]$ sudo openssl dhparam -check -out /etc/pki/tls/private/dhparam.pem -rand /dev/urandom 4096
DH parameters appear to be ok.

Set correct permissions.

[operator@gitea ~]$ sudo chmod 440 /etc/pki/tls/private/dhparam.pem

Disable TLS session tickets and enable the addition of OCSP responses to TLS negociation.

[operator@gitea ~]$ sudo sed -i "214i SSLSessionTickets Off" /etc/httpd/conf.d/ssl.conf
[operator@gitea ~]$ sudo sed -i "215i SSLUseStapling On" /etc/httpd/conf.d/ssl.conf

Accept all protocols except those before TLSv1.2.

[operator@gitea ~]$ sudo sed -i "s/#SSLProtocol.*/SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1/" /etc/httpd/conf.d/ssl.conf

Generate TLS certificate (local server only)

Generate the CA private key

Generate a private key for a curve. Here, I use the curve secp521r1 but you can use another one by executing openssl ecparam -list_curves.

[operator@gitea ~]$ sudo openssl ecparam -check -name secp521r1 -genkey -noout -out /etc/pki/tls/private/CAkey.pem -rand /dev/urandom
checking elliptic curve parameters: ok

Generate the CA certificate

The CA root certificate will have 3650 days (10 years lifetime).

[operator@gitea ~]$ sudo openssl req -utf8 -new -x509 -days 3650 -key /etc/pki/tls/private/CAkey.pem -out /etc/pki/tls/certs/CAcert.pem -rand /dev/urandom -subj "/CN=Gitea root CA/"

Generate Gitea private key

Generate a private key for a curve. Here, I use the curve secp384r1 but you can use another one by executing openssl ecparam -list_curves.

[operator@gitea ~]$ sudo openssl ecparam -check -name secp384r1 -genkey -noout -out /etc/pki/tls/private/privkey.pem -rand /dev/urandom
checking elliptic curve parameters: ok

Create a dedicated OpenSSL configuration file

Copy the following content in a file called openssl.cnf. In CN = and DNS.1 =, be sure to put the Gitea domain name, heere gitea.local.

[req]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[req_distinguished_name]
CN = gitea.local

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = gitea.local

Generate CSR based on the OpenSSL configuration file

[operator@gitea ~]$ sudo openssl req -new -key /etc/pki/tls/private/privkey.pem -out /tmp/gitea.local.csr -rand /dev/urandom -config openssl.cnf

Generate the X.509 certificate with SAN

Certificate will have 365 days (1 year lifetime).

[operator@gitea ~]$ sudo openssl x509 -req -days 365 -in /tmp/gitea.local.csr -CA /etc/pki/tls/certs/CAcert.pem -CAkey /etc/pki/tls/private/CAkey.pem -CAcreateserial -out /etc/pki/tls/certs/gitea.local.pem -rand /dev/urandom -extensions req_ext -extfile openssl.cnf
Signature ok
subject=CN = gitea.local
Getting CA Private Key

Check the SAN extension

[operator@gitea ~]$ openssl x509 -in /etc/pki/tls/certs/gitea.local.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name:
    DNS:gitea.local

Don’t forget to add the CA root certificate to your certificate store so that the certificate linked to the gitea.local domain name is validateur without error. You can retrieve it via this command.

[operator@gitea ~]$ cat /etc/pki/tls/certs/CAcert.pem

This command should return something like this.

-----BEGIN CERTIFICATE-----
MIICDTCCAW6gAwIBAgIUC1bvIYqIgFJAbb4JROhkmIcF4W8wCgYIKoZIzj0EAwIw
GDEWMBQGA1UEAwwNR2l0ZWEgcm9vdCBDQTAeFw0yMjA4MTcyMTE1MjNaFw0zMjA4
MTQyMTE1MjNaMBgxFjAUBgNVBAMMDUdpdGVhIHJvb3QgQ0EwgZswEAYHKoZIzj0C
AQYFK4EEACMDgYYABAFAw2jeHB4d7oOK7gkV4F5xZI9RaTfCEli3incAzaJUF7JX
rytAtqQ2ogFaHY0ugH0YpznfV2wk8q+KUCTS6DDacAEj+lPvtJyIdl/fYG+apa0V
xd2xY/s5XnLpufKpOo9lyuqIGLBm3AY2dmEOZ31kKY8cfjyIBK/URRZ0W6Wq2Na3
g6NTMFEwHQYDVR0OBBYEFCpFCCu03GkAHG09YDeVGbiZ7BgdMB8GA1UdIwQYMBaA
FCpFCCu03GkAHG09YDeVGbiZ7BgdMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E
AwIDgYwAMIGIAkIAmDnASKvExNnM4MPQp5HIGWynUqCb2IcciDApGM0EU7erfadL
aR+b0rXEFTdGbEG5mdE5uiYVYi51yJ71Qee4TFMCQgEsJlXv+rcpBJ2jT0vKX0FU
GgZu6jN2arTiZFkjgphouZS5hoiGXBCoJxZATyr5pwjQGOIaiRvG4iU6vcIYTiCz
lQ==
-----END CERTIFICATE-----

Copy this text and paste it in a file named CAcert.pem, this is the file you have to import in your trust store.

Set correct permissions

[operator@gitea ~]$ sudo chmod 440 /etc/pki/tls/private/CAkey.pem
[operator@gitea ~]$ sudo chmod 644 /etc/pki/tls/certs/CAcert.pem
[operator@gitea ~]$ sudo chmod 440 /etc/pki/tls/private/privkey.pem
[operator@gitea ~]$ sudo chmod 644 /etc/pki/tls/certs/gitea.local.pem

Virtual host configuration

Create the reverse proxy configuration into /etc/httpd/conf.d/vhost.conf.

<VirtualHost *:80>
    ServerName 192.168.2.61
    Redirect permanent / https://gitea.local
</VirtualHost>

<VirtualHost *:443>
    ServerName 192.168.2.61
    Redirect permanent / https://gitea.local
</VirtualHost>

<VirtualHost *:443>
    ServerName gitea.local:443
    ServerAlias gitea.local
    ServerAdmin contact@gitea.local

    SSLCertificateFile /etc/pki/tls/certs/gitea.local.pem
    SSLCertificateKeyFile /etc/pki/tls/private/privkey.pem
    SSLCACertificateFile /etc/pki/tls/certs/CAcert.pem

    Protocols h2 http/1.1

    ErrorLog /var/log/gitea/error_log
    CustomLog /var/log/gitea/access_log combined

    ProxyPreserveHost On
    ProxyRequests Off
    AllowEncodedSlashes NoDecode
    ProxyPass / http://localhost:3000/ nocanon
    ProxyPassReverse / http://localhost:3000/
</VirtualHost>

Request a Let’s Encrypt certificate (public server only)

In case you wish to obtain free certificates (via Let’s Encrypt), two methods are available to you. Luck is on your side, you will find here the method to obtain a certificate from Let’s Encrypt, and here a slightly more advanced method that allows you to obtain a certificate based on elliptic curve cryptography (still via Let’s Encrypt).

The reverse proxy configuration is the exact same. Things you have to modify are the location of the certificate, private key and the chain of trust, but it is documented in the article previously mentionned.

Create the logs directory

[operator@gitea ~]$ sudo mkdir -p /var/log/gitea

Enable and start HTTPD service

[operator@gitea ~]$ sudo systemctl enable --now httpd

Modify authorized ports

[operator@gitea ~]$ sudo firewall-cmd --add-port={80/tcp,443/tcp} --permanent
[operator@gitea ~]$ sudo firewall-cmd --reload

Finish installation

Open your web browser and go to the domain that you’ve defined, for me it’s https://gitea.local, and follow these steps:

In the Optional Settings:

Please note that all values can be changed later on the configuration file.

After installation is done, it is recommended to set rights to read-only.

[operator@gitea ~]$ sudo chmod 750 /etc/gitea
[operator@gitea ~]$ sudo chmod 640 /etc/gitea/app.ini

Configure git

Once you’ve created a standard user on Gitea (e.g. John Doe <john.doe@gitea.local>), you want to be able to push your commits, tags and more with the git command. Here’s how to do it.

First make sure that the basic configuration is done: user.name and user.email must be defined.

[adrien@laptop ~]$ git config --global user.name "John Doe"
[adrien@laptop ~]$ git config --global user.email john.doe@gitea.local

Next, generate a SSH key pair to authenticate yourself and add the private key into the SSH agent.

[adrien@laptop ~]$ ssh-keygen -t ed25519 -a 100 -f ~/.ssh/gitea.local -C git
[adrien@laptop ~]$ ssh-add ~/.ssh/gitea.local

Finally, copy the following snippet into ~/.ssh/config. Make sure to use the user git.

Host	gitea.local
	Hostname gitea.local
	User git
	Port 22
	IdentityFile ~/.ssh/gitea.local

Remember to import the public key (~/.ssh/gitea.pub) into your Gitea account.

Now, as soon as you perform actions with the git command (clone, commit…) the passphrase defined at the moment of the SSH key pair generation will be asked for instead of your username-password pair.

Recommendations

Backup

I don’t bother to explain the usefulness of a backup, so here is how to make one.

[operator@gitea ~]$ sudo su - git -c "/usr/local/bin/gitea dump --verbose --skip-custom-dir --skip-log --config /etc/gitea/app.ini --work-path /home/git"

As defined by the --work-path flag, the backup is in the /home/git directory. You can now retrieve it locally or move it to another server by encrypting the archive (which I advise you to do).

Restore

If you want to restore a backup, it is quite simple. First, stop Gitea service and extract the archive.

[operator@gitea ~]$ sudo systemctl stop gitea
[operator@gitea ~]$ unzip -q gitea-dump-1633005981.zip

Check its content.

[operator@gitea ~]$ ls
app.ini  data  gitea-db.sql  gitea-dump-1633005981.zip  repos

Once you have extracted the archive, you just have to move the files or folders to the proper locations.

[operator@gitea ~]$ sudo rm -rf /var/lib/gitea/data
[operator@gitea ~]$ sudo mv app.ini /etc/gitea/app.ini
[operator@gitea ~]$ sudo mv data/ /var/lib/gitea/
[operator@gitea ~]$ sudo mv repos/ /var/lib/gitea/data/gitea-repositories/
[operator@gitea ~]$ sudo chown -R git: /var/lib/gitea/data

Lastly, for the database, you have to delete the current database and recreate it.

[operator@gitea ~]$ sudo -iu postgres
[operator@gitea ~]$ psql
postgres=# DROP DATABASE giteadb;
DROP DATABASE
postgres=# \q

Recreate the database.

[postgres@gitea ~]$ createdb -E UTF8 -O gitea --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8' -T template0 giteadb
[postgres@gitea ~]$ exit
logout

Now, you can restore your database and start Gitea.

[operator@gitea ~]$ psql -h localhost -U gitea -f gitea-db.sql giteadb
Password for user gitea:
[operator@gitea ~]$ sudo systemctl start gitea

Update Gitea

You will find here a script I wrote to update the Gitea binary. The steps of implementation are explained in the README.md file.

Disable Swagger UI

Gitea uses Swagger to manage the API. Swagger is shipped with an (pretty cool) interface that lists the available endpoints. Obviously, you can disable this interface. It’s not really an additional measure of security because endpoints are easily found on the Internet. Add the following snippet into /etc/gitea/app.ini.

[api]
ENABLE_SWAGGER = false

Gitea can display some information about time of template execution, Gitea and Go version, it’s a good idea to hide this kind of stuff.

[other]
SHOW_FOOTER_VERSION		= false
SHOW_FOOTER_TEMPLATE_LOAD_TIME	= false

Set log mode

I advise you to activate the logs to be able to use Fail2ban correctly.

[operator@gitea ~]$ sudo sed -i "s/MODE      = console/MODE      = file/" /etc/gitea/app.ini

Protection against brute-force with Fail2ban

[operator@gitea ~]$ sudo dnf -y install fail2ban-server fail2ban-firewalld

First, we want to ban brute-force user login attempts. Copy the following snippet into /etc/fail2ban/filter.d/gitea.conf.

[INCLUDES]
before = common.conf

[Definition]
failregex = .*Failed authentication attempt for .* from <ADDR>
ignoreregex =

Create the jail by copying the following snippet into /etc/fail2ban/jail.d/gitea.local.

[gitea]
enabled = true
port = 80,443
filter = gitea
action = iptables-allports[name=gitea]
logpath = /var/lib/gitea/log/gitea.log
maxretry = 3
bantime = 86400
findtime = 3600

Once again, if SELinux is enabled Fail2ban will not start. You must change the log folder’s SELinux context.

[operator@gitea ~]$ sudo semanage fcontext -a -t var_log_t /var/lib/gitea/log/gitea.log
[operator@gitea ~]$ sudo restorecon -v /var/lib/gitea/log/gitea.log

Start the Fail2ban server and enable it at boot.

[operator@gitea ~]$ sudo systemctl enable --now fail2ban

Check the login attempts jail.

[operator@gitea ~]$ sudo fail2ban-client status gitea
Status for the jail: gitea
|- Filter
|  |- Currently failed: 0
|  |- Total failed:	0
|  `- File list:	/var/lib/gitea/log/gitea.log
|
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

You can try to log in with bad credentials and execute a new time the previous command, the Currently failed and Total failed values will increment. Don’t abuse, you’ll be block otherwise!

Finally, you can unban an IP with the following command.

[operator@gitea ~]$ sudo fail2ban-client set gitea unbanip <IP>

Setup logs rotation

[operator@gitea ~]$ sudo dnf -y install logrotate

Copy the following snippet into /etc/logrotate.d/gitea.

/var/lib/gitea/log/gitea.log {
        missingok
        notifempty
        compress
        weekly
        rotate 7
        delaycompress
        create 0644 git git
}

This configuration file contains:

You can execute a dry-run to see what logrotate would do if it was actually executed now.

[operator@gitea ~]$ sudo logrotate -d /etc/logrotate.d/gitea

Finally, logrotate creates an entry in /etc/cron.daily/logrotate to execute the configured tasks.

We just finished working out how to set up Gitea. You can now do what you are used to do on GitHub!