Preface

In combination with the Raspberry Pi OS for a server and Using XCA to create private SSL certificates and PiSQL articles, this article describes setting up a Raspberry Pi Model B+ as a private Gitea (lightweight Git hosting) server. It assumes a separate PostgreSQL server is available on the network and does not use external storage (just a reasonably large SD card). Finally, this is intended only for internal use and is not expected to be publicly facing.

A caveat

This will work well for ordinary repositories, but for very large repositories like the Linux Kernel (>1 GB) the Pi is simply not powerful enough.

This is a limitation of Git rather than Gitea (even bare git operations on kernel repos do not fare well on a Pi).

Preliminaries

Prepare the server and certificates

  1. Setup a Pi following the Raspberry Pi OS for a server article. For this server the author skipped the external storage related steps and packages as he is operating the server entirely from a large and fast enough SD card.
  2. Create internal SSL CA and server certificate and key by following the Using XCA to create private SSL certificates article. (As with the PiSQL article, we require server certificates for the server (e.g. gitea.example.com); this time we do not need to generate a CA (Certificate Authority) but can use the existing one).
  3. We also need to generate a ‘client’ certificate for this host, which uses the same procedure, except that instead of selecting the ‘[default] TLS_server’ template, we select the ‘[default] TLS_client’ template. For the client certificate I recommend using a name such gitea@gitea.example.com for the CN — you can still add the server DNS name as Subject Alternative Names (x509v3 SAN). Note that on the client certificate at one SAN and/or the CN must be the username gitea uses to connect to the database.
  4. Increase the available amount of swap:
    1. sudo dd if=/dev/zero of=/var/swap2.img bs=1M count=1024

    2. sudo chmod 600 /var/swap2.img

    3. sudo mkswap /var/swap2.img

    4. sudoedit /etc/fstab and add a line such as:

      /var/swap2.img none swap defaults 0 0
      
    5. Enable the swap: sudo swapon -a

    6. Verify it is enabled: cat /proc/swaps

Prepare your clients to use SSL to the server

Because we are using a private CA your desktop and other Git clients need to be told to trust the private CA.

  1. On any Debian/Ubuntu workstation that needs to access the private CA, copy the private CA certificate (e.g. ca-private.example.com) to /usr/local/share/ca-certificates and execute update-ca-certificates

  2. Also on any Debian/Ubuntu workstation for which Firefox needs to access the server:

    mkdir -p /etc/firefox/policies
    sudoedit /etc/firefox/policies/policies.json
    

    In policies.json add:

    {
      "policies": {
        "Certificates": {
          "Install": [
            "/usr/local/share/ca-certificates/ca-private.example.com.crt"
          ]
        }
      }
    }
    
  3. On any Windows workstation that needs to access the private CA,

    1. Install the private CA into the system certificate store

      1. Windows 10 install certificate dialogue
        Double-click on ca-private-example.com.crt, select ‘Install certificate’ and click ‘OK’
      2. Select Local Machine in install certificate wizard
        For ‘Store Location’ select ‘Local Machine’ and click ‘Next’. You may be prompted for your administrative credentials.
      3. Selection of location to install certificate in the install certificate wizard
        Select ‘Place all certificates in the following store’ and click ‘Browse…’
      4. Selection of which system-wide store to use in the install certificate wizard
        Select ‘Trusted Root Certification Authorities’ and click ‘OK’
      5. Confirmation page for install certificate wizard
        Confirm the details presented and click ‘Finish’
    2. For making the CA available for recent Firefox system-wide:

      1. Create a directory called C:\\ProgramData\\FirefoxCertificates

      2. Copy ca-private.example.com.crt to C:\\ProgramData\\FirefoxCertificates

      3. Create a directory called distribution in C:\\Program Files\\Mozilla Firefox, and in the distribution directory add a file called policies.json containing:

        {
          "policies": {
            "Certificates": {
              "Install": [
                "C:\\ProgramData\\FirefoxCertificates\\ca-private.example.com.crt"
              ]
            }
          }
        }
        

      See Also Mozilla’s GitHub repository for policy templates

Obtain, verify and ‘install’ Gitea

From the Gitea download area, select the current release and linux-arm-6 binaries. At the present time we want the following files: https://dl.gitea.io/gitea/1.14.3/gitea-1.14.3-linux-arm-6.xz, https://dl.gitea.io/gitea/1.14.3/gitea-1.14.3-linux-arm-6.xz.asc, and https://dl.gitea.io/gitea/1.14.3/gitea-1.14.3-linux-arm-6.xz.sha256.

  1. You could for example use the following trio of commands:

    wget https://dl.gitea.io/gitea/1.14.3/gitea-1.14.3-linux-arm-6.xz
    wget https://dl.gitea.io/gitea/1.14.3/gitea-1.14.3-linux-arm-6.xz.asc
    wget https://dl.gitea.io/gitea/1.14.3/gitea-1.14.3-linux-arm-6.xz.sha256
    
  2. Now verify the correctness of the download:

  3. Execute

    sha256sum --ignore-missing -c gitea-1.14.3-linux-arm-6.xz.sha256
    
  4. And that it it matches the version intended by the authors (signed by them)

    gpg --keyserver keyserver.ubuntu.com --recv-keys CC64B1DB67ABBEECAB24B6455FC346329753F4B0
    gpg --verify gitea-1.14.3-linux-arm-6.xz.asc gitea-1.14.3-linux-arm-6.xz
    

    As long as the report is of a good signature, you can ignore:

    gpg: WARNING: This key is not certified with a trusted signature!
    gpg:          There is no indication that the signature belongs to the owner.
    

    as the so-called Web of Trust which would resolve that message has not come to exist.

  5. Now decompress the gitea binary:

    xz -d gitea-1.14.3-linux-arm-6.xz
    
  6. And copy it to where you will use it and make it executable

    sudo cp gitea-1.14.3-linux-arm-6 /usr/local/bin/gitea
    sudo chmod 755 /usr/local/bin/gitea
    
  7. You can now remove the gitea-1.1.4.3-linux-arm-6* files.

Prepare the environment

Create a Gitea user

sudo addgroup --system gitea
sudo adduser  --system --home /srv/gitea/home --shell /bin/bash --gecos "" --ingroup gitea --disabled-password --disabled-login gitea

Add the PostgreSQL client (optional)

We use this to create and prepare the database in a way that verifies that connections will work for the gitea process (i.e. when going live).

sudo apt install -y postgresql-client

Copy the SSL client keys to the Gitea user for PostgreSQL use

Assuming you have copied your client gitea@gitea.example.com.crt and gitea@gitea.example.com.pem to your admin user, as well as the ca certificate (which we will call ‘root.crt’), and that you are currently in your admin user account:

  1. sudo cp gitea@gitea.example.com* root.crt /srv/gitea/
  2. sudo chown gitea:gitea /srv/gitea/gitea@gitea.example.com* /srv/gitea/root.crt
  3. You can now remove the client cert and key from your admin user account: rm gitea@gitea.example.com*
  4. sudo -u gitea -sH
  5. cd ~
  6. chmod 700 .
  7. mkdir .postgresql
  8. chmod 700 .postgresql
  9. mv gitea@gitea.example.com* root.crt .postgresql
  10. cd .postgresql
  11. mv gitea@gitea.example.com.crt postgresql.crt
  12. mv gitea@gitea.example.com.pem postgresql.key
  13. cp root.crt /usr/local/share/ca-certificates/ca-private.gitea.internal.com
  14. update-ca-certificates
  15. cd ..

NB Keep this session open, we’ll come back to it

On the Database server, create user and database for Gitea

  1. Login as your admin user to your database server

  2. sudo su - postgres

  3. createuser -P gitea@gitea.example.com

  4. Enter a new strong password when prompted (you will need to enter it twice).

  5. For C.UTF-8 below substitute the appropriate UTF-8 locale:

    createdb -O gitea@gitea.example.com --encoding UTF8 --locale C.UTF-8 --template template0 giteadb
    
  6. exit

  7. exit

See also: Gitea documentation on setting up PostgreSQL for Gitea

Back on the Gitea server, test DB connection

In the session as user gitea from above and assuming your db server is named pisql.example.com:

psql "postgres://gitea%40gitea.example.com@pisql.example.com/giteadb?sslmode=verify-full"

(%40 is the URL encoding of the ‘@’ symbol in the username)

Enter the database user’s password when prompted, and you should get a standard ‘psql’ prompt, such as:

psql (11.12 (Raspbian 11.12-0+deb10u1), server 11.11 (Raspbian 11.11-0+deb10u1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

giteadb=>

Enter \l to see the list of databases, and \q to quit.

Back to admin, create the needed directories

exit the gitea user shell so that you are back into your admin user shell.

sudo mkdir -p /srv/gitea/data/{custom,data,log,lfs,repositories}
sudo mkdir -p /etc/gitea
sudo chown -R gitea:gitea /srv/gitea/data /etc/gitea
sudo chmod 750 /srv/gitea/data /etc/gitea
sudo chown root /etc/gitea
sudo touch /etc/gitea/app.ini
sudo chmod 640 /etc/gitea/app.ini
sudo chown root:gitea /etc/gitea/app.ini

Configure Gitea

Create the config file

  1. Create and record the secrets you will need:

    1. gitea generate secret SECRET_KEY
    2. gitea generate secret INTERNAL_TOKEN
    3. gitea generate secret LFS_JWT_SECRET
  2. sudoedit /srv/gitea-config/app.ini

  3. Copy your server certificate and key to /etc/gitea:

    1. Assuming they are in your admin user’s home directory:

      sudo cp gitea.example.com.crt /etc/gitea/
      sudo cp gitea.example.com.key /etc/gitea/
      sudo -sH
      cd /etc/gitea
      chown root:gitea gitea.example.com.crt
      chown root:gitea gitea.example.com.key
      chmod 640 gitea.example.com.key
      chmod 644 gitea.example.com.crt
      
  4. Add a config such as (substituting the secrets above, and the database password):

    APP_NAME = Example Gitea
    RUN_USER = gitea
    RUN_MODE = prod
    
    [security]
    INSTALL_LOCK       = true
    PASSWORD_HASH_ALGO = pbkdf2
    SECRET_KEY         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    INTERNAL_TOKEN     = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    
    [database]
    DB_TYPE  = postgres
    HOST     = pisql.example.com:5432
    NAME     = giteadb
    USER     = gitea@gitea.example.com
    PASSWD   = XXXXXXXXXXXXXXXXXXXXXXX ; USE `XXXXXXXXXXXXXXX` if special characters appear in PASSWD
    SSL_MODE = verify-full
    CHARSET  = utf8
    LOG_SQL  = false
    
    [repository]
    ROOT = /srv/gitea-data/repositories
    DEFAULT_PUSH_CREATE_PRIVATE = false
    ENABLE_PUSH_CREATE_ORG = false
    ENABLE_PUSH_CREATE_USER = false
    DEFAULT_BRANCH = main
    
    [server]
    SSH_DOMAIN       = gitea.example.com
    DOMAIN           = gitea.example.com
    REDIRECT_OTHER_PORT = true
    PORT_TO_REDIRECT = 80
    HTTP_PORT        = 443
    PROTOCOL         = https
    ROOT_URL         = https://gitea.example.com/
    DISABLE_SSH      = false
    SSH_PORT         = 22
    LFS_START_SERVER = true
    LFS_CONTENT_PATH = /srv/gitea-data/lfs
    LFS_JWT_SECRET   = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    OFFLINE_MODE     = true
    START_SSH_SERVER = true
    CERT_FILE        = /srv/gitea-config/gitea.example.com.crt
    KEY_FILE         = /srv/gitea-config/gitea.example.com.key
    
    [mailer]
    ENABLED = false
    
    [service]
    REGISTER_EMAIL_CONFIRM            = false
    ENABLE_NOTIFY_MAIL                = false
    DISABLE_REGISTRATION              = true
    ALLOW_ONLY_EXTERNAL_REGISTRATION  = false
    ENABLE_CAPTCHA                    = false
    REQUIRE_SIGNIN_VIEW               = false
    DEFAULT_KEEP_EMAIL_PRIVATE        = false
    DEFAULT_ALLOW_CREATE_ORGANIZATION = false
    DEFAULT_ENABLE_TIMETRACKING       = false
    NO_REPLY_ADDRESS                  = noreply.example.com
    
    [oauth2]
    JWT_SECRET = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    
    [session]
    PROVIDER = db
    
    [indexer]
    ISSUE_INDEXER_TYPE = db
    
    [log]
    MODE                 = console
    LEVEL                = info
    ROOT_PATH            = /srv/gitea-data/log
    COLORIZE             = false
    # Router is quite noisy and not usually needed on a private server
    # Log to a file instead of journal (via console)
    ROUTER               = file
    
    DISABLE_GRAVATAR        = true
    ENABLE_FEDERATED_AVATAR = false
    
    [openid]
    ENABLE_OPENID_SIGNIN = false
    ENABLE_OPENID_SIGNUP = false
    
    [other]
    SHOW_FOOTER_VERSION = true
    SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
    

If you omit PostgreSQL and use SQLite3

Replace the [database] section above with:

[database]
DB_TYPE  = sqlite3
NAME     = gitea
LOG_SQL  = false

Add Fail2Ban configs (optional)

  1. Create /etc/fail2ban/filter.d/gitea.conf

    # gitea.conf
    [Definition]
    failregex =  .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
    ignoreregex =
    
  2. Create /etc/fail2ban/jail.d/gitea.conf

    [gitea]
    enabled = true
    filter = gitea
    logpath = /var/log/syslog
    maxretry = 10
    findtime = 3600
    bantime = 900
    action = iptables-allports
    
  3. Restart fail2ban

    sudo systemctl restart fail2ban
    
  4. Verify fail2ban accepted your changes

    sudo systemctl status -l fail2ban
    

Add systemd service file

Modified from Gitea example systemd service file

Copy the following file to /etc/systemd/system/gitea.service

[Unit]
Description=Gitea (Git with a cup of tea)
After=syslog.target
After=network.target
###
#
#Requires=memcached.service
#Requires=redis.service
#

[Service]
# Modify these two values and uncomment them if you have
# repos with lots of files and get an HTTP error 500 because
# of that
###
#LimitMEMLOCK=infinity
#LimitNOFILE=65535
RestartSec=2s
Type=simple
User=gitea
Group=gitea
WorkingDirectory=/srv/gitea/data/
# If using Unix socket: tells systemd to create the /run/gitea folder, which will contain the gitea.sock file
# (manually creating /run/gitea doesn't work, because it would not persist across reboots)
#RuntimeDirectory=gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
Environment=USER=gitea HOME=/srv/gitea/home GITEA_WORK_DIR=/srv/gitea/data
###
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
###

[Install]
WantedBy=multi-user.target

Create a wrapper script for CLI usage

  1. Copy the following file to /usr/local/bin/gitea-wrapper

  2. sudo chmod 755 /usr/local/bin/gitea-wrapper

    #!/bin/bash
    
    export USER=gitea
    export HOME=/srv/gitea/home
    export GITEA_WORK_DIR=/srv/gitea/data
    
    if [ "$(id -un)" != "gitea" ]; then
      echo "Must be run as user 'gitea'"
      exit 1
    fi
    
    exec /usr/local/bin/gitea --config /etc/gitea/app.ini "$@"
    

Initialize the database

sudo -u gitea gitea-wrapper migrate

Create an admin user

sudo -u gitea gitea-wrapper admin user create --username giteadmin --email you@example.com --admin --must-change-password --random-password --random-password-length 20

A random password will be generated for first login (at which time the user must change their password).

Configure firewall to allow alternate SSH port

sudo ufw allow in on eth0 proto tcp from any to any port 22172

Configure sshd to use alternate SSH port

We’re going to use port 22 for Git SSH, so make admin SSH a separate port.

  1. sudoedit /etc/ssh/sshd_config
  2. Add the line Port 22172 and save and exit.
  3. sudo systemctl restart ssh — From now on, to administer the host we will need to tell SSH to use port 22172 (e.g. ssh -p 22172 pi@gitea.example.com).
  4. exit
  5. Log back in as admin using the new port ssh -p 22172 pi@gitea.example.com

Configure firewall to allow HTTP(S) connections

sudo ufw allow in on eth0 from any to any app "WWW Full"

Enable and launch Gitea

sudo systemctl enable --now gitea

Verify Gitea status

sudo systemctl status -l gitea

ss -ltn

It could be some minutes before Gitea is ready to accept connections, at which point ports 80, 443, and 22 will be accepting connections.

When Gitea is ready, login as admin user.

Gitea operational

You should now have a working Gitea Server

Configure backups

Assuming the use of restic as in the Pi OS for a server guide you could

  1. sudo -u gitea -sH

  2. cd ~

  3. mkdir restic-files

  4. cd restic-files

  5. chmod 700 .

  6. touch password-file

  7. chmod 600 password-file

  8. sensible-editor password-file

  9. In the editor, add a strong password (e.g. 30 alphanumeric and special characters), then save and close (having a file with the password not ideal, but avoiding it is rather complicated, and out of scope for this article).

  10. If using SFTP for backups, create a passwordless SSH keypair using:

    1. ssh-keygen -t rsa -f restic-gitea@piserver -C restic-gitea@piserver -N ''
    2. Copy the contents of restic-gitea@piserver.pub to your destination’s authorized_keys file.
  11. Initialize your destination restic repository

  12. Do an initial backup

    1. (assuming you have configured, ~/.ssh/config so that restic-gitea@backupserver.example.com uses the restic-gitea@piserver created above:

      /usr/local/bin/gitea-wrapper dump --custom-path /srv/gitea/data/custom --work-path /srv/gitea/data --type tar.gz -f - 2>/dev/null | restic -r sftp:restic-gitea@backupserver.example.com:/path/to/repo --password-file ~/restic-files/password-file backup --stdin --stdin-filename /gitea-dump.tar.gz
      
  13. Now create a crontab entry to do this every four hours (you could of course adjust the frequency for your needs):

    1. crontab -e

    2. Add an entry such as:

      23  */4  *   *   *  /usr/local/bin/gitea-wrapper dump --custom-path /srv/gitea/data/custom --work-path /srv/gitea/data --type tar.gz -f - 2>/dev/null | restic -r sftp:restic-gitea@backupserver.example.com:/path/to/repo --quiet --password-file ~/restic-files/password-file backup --stdin --stdin-filename /gitea-dump.tar.gz 2>&1 | logger -t restic
      
    3. Save and exit the editor

  14. exit