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

sudo dnf -y update

Add the MariaDB repository

Copy the following snippet into /etc/yum.repos.d/MariaDB.repo.

[mariadb]
name=MariaDB
baseurl=https://yum.mariadb.org/10.6/centos8-amd64/
module_hotfixes=1
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

Disable MariaDB history file

It’s a good idea to prevent MariaDB from writing a history file. The MariaDB history file contains all the commands you type across all sessions. The default MariaDB history file is ~/.mysql_history. As this is an obvious security risk, it is a good idea to disable this file.

echo "export MYSQL_HISTFILE=/dev/null" | tee -a ~/.bash_profile
source ~/.bash_profile

This directs your MariaDB history to /dev/null.

Install MariaDB and required packages

sudo dnf -y install MariaDB-server MariaDB-client git epel-release httpd mod_ssl vim firewalld policycoreutils-python-utils

Enable and start MariaDB service

sudo systemctl enable --now mariadb

Install MariaDB

sudo mariadb-secure-installation
<ENTER>
n
Y
<root password>
<confirm>
Y
Y
Y
Y

Configure database

mariadb -u root -p
Enter password: <root user's password>

Create the user

Create the hash of its password

Choose a really strong password instead of P@ssw0rd.

MariaDB [(none)]> SELECT PASSWORD("P@ssw0rd");
+-------------------------------------------+
| PASSWORD("P@ssw0rd")                      |
+-------------------------------------------+
| *8232A1298A49F710DBEE0B330C42EEC825D4190A |
+-------------------------------------------+
1 row in set (0.000 sec)

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

tr -cd "[:alnum:]" < /dev/urandom | fold -w 39 | head -n1

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

Create the user with the hashed password

MariaDB [(none)]> CREATE USER "gitea" IDENTIFIED BY PASSWORD "*8232A1298A49F710DBEE0B330C42EEC825D4190A";
Query OK, 0 rows affected (0.008 sec)

Create the database

MariaDB [(none)]> CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
Query OK, 1 row affected (0.001 sec)

Grant all privileges

MariaDB [(none)]> GRANT ALL PRIVILEGES ON giteadb.* TO 'gitea';
Query OK, 0 rows affected (0.011 sec)

Flush privileges

MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.001 sec)

Exit

MariaDB [(none)]> EXIT;
Bye

Test the connection

mariadb -u gitea -p owncloud
Enter password: <gitea user's password>
MariaDB [gitea]> EXIT;
Bye

Download the binary

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

curl -LOsSf https://dl.gitea.io/gitea/1.16.5/gitea-1.16.5-linux-amd64
sudo mv gitea-1.16.5-linux-amd64 /usr/local/bin/gitea
sudo chmod +x /usr/local/bin/gitea

Create user to run Gitea server

sudo adduser --system --shell /bin/bash --comment "Gitea server" --user-group -m git

Create the directory structure

sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo chown -R git:git /var/lib/gitea/
sudo chmod -R 750 /var/lib/gitea/
sudo mkdir /etc/gitea
sudo chown root:git /etc/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

sudo systemctl daemon-reload

Enable and start Gitea service

sudo systemctl enable --now gitea

SELinux stuff

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

sudo semanage fcontext -a -t bin_t /usr/local/bin/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.

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 configurations

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

sudo sed -i "352i ServerTokens Prod" /etc/httpd/conf/httpd.conf

General TLS configurations

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.

sudo openssl dhparam -out /etc/pki/tls/private/dhparam.pem 4096
sudo chmod 440 /etc/pki/tls/private/dhparam.pem

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

sudo sed -i "214i SSLSessionTickets Off" /etc/httpd/conf.d/ssl.conf
sudo sed -i "215i SSLUseStapling On" /etc/httpd/conf.d/ssl.conf

Accept all protocols except those before TLSv1.2.

sudo sed -i "s/#SSLProtocol.*/SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1/" /etc/httpd/conf.d/ssl.conf

Generate a self-signed certificate (local server only)

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.

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 self-signed certificate. Feel free to personalize the answers to the questions asked. This self-signed certificate is used for testing purposes so it is not essential that the information given is correct.

sudo openssl req -utf8 -new -x509 -key /etc/pki/tls/private/privkey.pem -out /etc/pki/tls/certs/cert.pem -days 365 -rand /dev/urandom
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letters code) [XX]:FR
State or Province Name (full name) []:Auvergne-Rhone-Alpes
Locality Name (eg, city) [Default City]:Lyon
Organizational Name (eg, company) [Default Company Ltd]:Illuad Technologies
Organizational Unit Name (eg, section) []:IT Security
Common Name (eg, your name or your server's hostname) []:gitea.local
Email Address []:contact@gitea.local

Set the correct permission.

sudo chmod 440 /etc/pki/tls/private/privkey.pem
sudo chmod 644 /etc/pki/tls/certs/cert.pem

Then, create the reverse proxy configuration into /etc/httpd/conf.d/vhost.conf.

<VirtualHost *:80>
    ServerName gitea.local:80
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

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

    SSLCertificateFile /etc/pki/tls/certs/cert.pem
    SSLCertificateKeyFile /etc/pki/tls/private/privkey.pem
    SSLCACertificateFile /etc/pki/tls/certs/cert.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

sudo mkdir -p /var/log/gitea

Enable and start HTTPD service

sudo systemctl enable --now httpd

Modify authorized ports

sudo firewall-cmd --add-port={80/tcp,443/tcp} --permanent
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/install, 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.

sudo chmod 750 /etc/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.

git config --global user.name "John Doe"
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.

ssh-keygen -t ed25519 -a 100 -f ~/.ssh/gitea.local -C git
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.

sudo su - git -c "/usr/local/bin/gitea dump --verbose --skip-custom-dir --skip-log --config /etc/gitea/app.ini --work-path /home/git"
[...]
2021/09/30 14:46:47 cmd/dump.go:264:runDump() [I] Dumping database...
2021/09/30 14:46:47 cmd/dump.go:31:addFile() [I] Adding file gitea-db.sql
2021/09/30 14:46:47 cmd/dump.go:276:runDump() [I] Adding custom configuration file from /etc/gitea/app.ini
2021/09/30 14:46:47 cmd/dump.go:31:addFile() [I] Adding file app.ini
2021/09/30 14:46:47 cmd/dump.go:283:runDump() [I] Skipping custom directory
2021/09/30 14:46:47 cmd/dump.go:304:runDump() [I] Packing data directory.../home/git/data
2021/09/30 14:46:47 cmd/dump.go:349:runDump() [I] Skip dumping log files
2021/09/30 14:23:11 cmd/dump.go:374:runDump() [I] Finish dumping in file gitea-dump-1633005981.zip

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 and extract the archive.

sudo systemctl stop gitea
unzip -q gitea-dump-1633005981.zip

Check its content.

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.

sudo rm -rf /var/lib/gitea/data
sudo mv app.ini /etc/gitea/app.ini
sudo mv data/ /var/lib/gitea/
sudo mv repos/ /var/lib/gitea/data/gitea-repositories/
sudo chown -R git:git /var/lib/gitea/data

Lastly, for the database, you have to delete the current database and recreate it. Start by listing the existing databases.

mariadb -uroot -p -e "SHOW DATABASES;"
Enter password: <root password>
+--------------------+
| Database           |
+--------------------+
| giteadb            |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

Drop the giteadb database.

mariadb -uroot -p -e "DROP DATABASE giteadb;"
Enter password: <root password>

Recreate the database.

mariadb -uroot -p -e "CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; GRANT ALL PRIVILEGES ON giteadb.* TO 'gitea'; FLUSH PRIVILEGES;"
Enter password: <root password>

Check to see if you’ve done your job right.

mariadb -uroot -p -e "SHOW CREATE DATABASE giteadb;"
Enter password:
+----------+------------------------------------------------------------------------------------------------+
| Database | Create Database                                                                                |
+----------+------------------------------------------------------------------------------------------------+
| giteadb  | CREATE DATABASE `giteadb` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ |
+----------+------------------------------------------------------------------------------------------------+

Now, you can restore your database and start Gitea.

mariadb --default-character-set=utf8mb4 --binary-mode -o -ugitea -p giteadb < gitea-db.sql
Enter password: <gitea password>
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.

sudo sed -i "s/MODE      = console/MODE      = file/" /etc/gitea/app.ini

Protection against brute-force with Fail2ban

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.

sudo semanage fcontext -a -t var_log_t /var/lib/gitea/log/gitea.log
sudo restorecon -Rv /var/lib/gitea/log/gitea.log

Start the Fail2ban server and enable it at boot.

sudo systemctl enable --now fail2ban

Check the login attempts jail.

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.

sudo fail2ban-client set gitea unbanip <IP>

Setup logs rotation

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.

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!