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:
- The manipulations are carried out on Rocky Linux 8.
- Throughout this post, we will access the Gitea server via its domain name
gitea.local
which is related to its local IP address. - In some snippets of code, the domain name
gitea.domain.fr
must be replaced by the one you assigned to your public Gitea server.
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:
- Choose
PostgreSQL
inDatabase type
. - Fill the
Password
field. - Fill the
Database Name
field withgiteadb
. - Fill the
Site Title
field with the title of your choice. - Fill the
Server Domain
field with the domain name (without the protocol), for me it’sgitea.local
. - Fill the
Gitea base URL
field with the domain name (with the protocol), for me it’shttps://gitea.local
.
In the Optional Settings
:
- Expand the
Email Settings
section and fill-in the fields if you want to enable e-mail service. - Expand the
Server and Third-Party Service Settings
section and check the features you want to use, uncheck the ones you are not going to use. - Expand the
Administrator Account Settings
section and fill-in the fields to create the administrator user. - Click on
Install Gitea
to install Gitea.
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
Disable footer information
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:
missingok
: if the log file is missing, go on to the next one without issuing an error message.notifempty
: do not rotate the log if it is empty.compress
: old versions of log files are compressed withGzip
.weekly
: log files are rotated if the current weekday is less than the weekday of the last rotation or if more than a week has passed since the last rotation.rotate 7
: log files are rotated count times before being removed.delaycompress
: postpone compression of the previous log file to the next rotation cycle. It can be used when some program cannot be told to close its logfile and thus might continue writing to the previous log file for some time.create 0644 git git
: immediately after rotation, the log file is created with the same name as the log file just rotated.
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!