How to properly secure Gitea with Let's Encrypt SSL cert?

Hello,

I’m fairly new at nginx configuration and my team has asked me to set up a web-facing Gitea cloud server.

I configured ufw to allow www (http, https), ssh, by default refuse all incoming connections and allow all outgoing connections. I did NOT add port 3000, so while formerly http://my-server.mydomain.com:3000 was working (proving that Gitea install worked and the app was running), this URL was properly refused only when I enabled ufw.

I followed the instructions How to Install Gitea on Ubuntu 22.04 to install Gitea and secure via a SSH cert from Let’s Encrypt.

but I’m having a problem:

The URL https : // my-server . mydomain . com is throwing a 404 (not found).

Any idea how I can get my Gitea properly responding via https?

Here’s the contents of my file /etc/nginx/sites-enabled/gitea (since certbot added its content to /etc/nginx/sites-enabled/default and Gitea added its content to /etc/nginx/sites-enabled/gitea, I tried to merge the two and removed default from my enabled sites. Obviously I missed something :frowning: ) :

upstream gitea {
    server localhost:3000;
}

server {

        # SSL configuration
        #


    root /var/lib/gitea/public;

        # Add index.php to the list if you are using PHP
        #index index.html index.htm index.nginx-debian.html;
    server_name my-server; # managed by Certbot
    access_log on;
    error_log on;


        location / {
                # First attempt to serve request as file, 
                # then redirect to gitea, then fall back to displaying a 404.
                # try_files maintain.html $uri $uri/index.html @node $uri/ =404;
                try_files maintain.html $uri $uri/index.html @node =404;
        }

    location @node {
      client_max_body_size 0;
      proxy_pass http://localhost:3000;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_max_temp_file_size 0;
      proxy_redirect off;
      proxy_read_timeout 120;
    }


    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/my-server.mydomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/my-server.mydomain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = my-server.mydomain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    access_log on;
    error_log on;

        listen 80 ;
        listen [::]:80 ;
    server_name my-server.mydomain.com;
    return 404; # managed by Certbot
}

Here’s the contents of my file /etc/gitea/app.ini :

[repository]
ROOT = /var/lib/gitea/data/gitea-repositories

[server]
SSH_DOMAIN = orb
DOMAIN = orb
HTTP_PORT = 3000
APP_DATA_PATH = /var/lib/gitea/data
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = F0Wn0xl80Bg-6Dl0KWCsNOu7wg1vjBfsAArUYqSOtiQ
OFFLINE_MODE = false
#ROOT_URL = http://orb:3000/
ROOT_URL = https://orb.ktechnology.ca
PROTOCOL = https

[lfs]
PATH = /var/lib/gitea/data/lfs

[mailer]
ENABLED = false

[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[cron.update_checker]
ENABLED = false

[session]
PROVIDER = file

[log]
MODE = console
LEVEL = info
ROOT_PATH = /var/lib/gitea/log

[repository.pull-request]
DEFAULT_MERGE_STYLE = merge

[repository.signing]
DEFAULT_TRUST_MODEL = committer

[security]
INSTALL_LOCK = true
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2OTg4NjY4NTh9.coxIV16Tz_8DTdDqd67Bqgv34bXzuFoHRg3i3a5QSnw
PASSWORD_HASH_ALGO = pbkdf2

[oauth2]
JWT_SECRET = y5tZr5C5-Xk8Wn6OaVbeO1Vw2TtoJ_KSdv7jF0I1NX4

Thanks in advance.

Ok, nailed it. I think it might help others to post my solution :

Contents of the nginx gitea configuration file :

upstream gitea {
    server localhost:3000;
}

server {

    # SSL configuration
    #
    # Note: You should disable gzip for SSL traffic.
    # See: https://bugs.debian.org/773332
    #
    # Read up on ssl_ciphers to ensure a secure configuration.
    # See: https://bugs.debian.org/765782
    #

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    root /var/lib/gitea/public;

    # Add index.php to the list if you are using PHP
    #index index.html index.htm index.nginx-debian.html;
    server_name my-server.mydomain.com; # managed by Certbot
    access_log /var/log/nginx/gitea_access.log;
    error_log /var/log/nginx/gitea_error.log;


    location / {
        client_max_body_size 900M;
        proxy_pass http://localhost:3000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_max_temp_file_size 0;
        proxy_read_timeout 120;
    }



    #ssl_session_cache   shared:SSL:10m;
    #ssl_session_timeout 10m;
    #keepalive_timeout   70;

    ssl_certificate /etc/letsencrypt/live/my-server.mydomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/my-server.mydomain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = my-server.mydomain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    access_log /var/log/nginx/gitea_access.log;
    error_log /var/log/nginx/gitea_error.log;

    listen 80 ;
    listen [::]:80 ;
    server_name my-server.mydomain.com;
    return 404; # managed by Certbot
}

Contents of the Gitea app.ini file [server] block (which was the only one that I had to change to make basic access work). Also, there were a few deprecated keys in my original post so I updated these as well :

[server]
SSH_DOMAIN = mydomain.com
DOMAIN = mydomain.com
HTTP_PORT = 3000
APP_DATA_PATH = /var/lib/gitea/data
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = <my-secret>
OFFLINE_MODE = false
ROOT_URL = http://localhost:3000/

Final update. I reworked the configuration to allow Gitea to run internally on http : //localhost : 3000 while exposing it to the world using https : //my-server . mydomain.com (which automatically redirects to https : //my-server . mydomain . com / gitea):

NGINX configuration

upstream gitea {
    server localhost:3000;
}

server {

    # SSL configuration
    #
    # Note: You should disable gzip for SSL traffic.
    # See: https://bugs.debian.org/773332
    #
    # Read up on ssl_ciphers to ensure a secure configuration.
    # See: https://bugs.debian.org/765782
    #

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    root /var/lib/gitea/public;

    # Add index.php to the list if you are using PHP
    #index index.html index.htm index.nginx-debian.html;
    server_name my-server.mydomain.com; # managed by Certbot
    access_log /var/log/nginx/gitea_access.log;
    error_log /var/log/nginx/gitea_error.log;


    # Check if the request is for /gitea/, if not, redirect to /gitea/
    # The 308 (Permanent redirect) will ensure the same HTTP verb
    # (GET, POST, etc) will be used when the URL is redirected.
    if ($request_uri !~ ^/gitea/) {
        return 308 https://$host/gitea$request_uri;
    }

    location /gitea/ {
        client_max_body_size 900M;
        proxy_pass http://gitea/; # Ensure the trailing slash is present
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_max_temp_file_size 0;
        proxy_read_timeout 120;
        proxy_redirect off;
        proxy_set_header X-Script-Name /gitea;
    }

    # This block will handle the static assets which are not found in the root directory
    location ~* ^/gitea/(css|js|fonts|img|less|vendor)/ {
        proxy_pass http://localhost:3000; # Ensure no trailing slash
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_max_temp_file_size 0;
        proxy_read_timeout 120;
        proxy_redirect off;
        proxy_set_header X-Script-Name /gitea;
    }


    ssl_certificate /etc/letsencrypt/live/my-server.mydomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/my-server.mydomain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = my-server.mydomain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    access_log /var/log/nginx/gitea_access.log;
    error_log /var/log/nginx/gitea_error.log;

    listen 80 ;
    listen [::]:80 ;
    server_name my-server.mydomain.com;
    return 404; # managed by Certbot
}     

Gitea configuration

APP_NAME = Gitea: Git with a cup of tea
RUN_USER = git
WORK_PATH = /var/lib/gitea
RUN_MODE = prod

[database]
DB_TYPE = mysql
HOST = 127.0.0.1:3306
NAME = gitea
USER = gitea
PASSWD = <gitea db admin password>
SCHEMA = 
SSL_MODE = disable
PATH = /var/lib/gitea/data/gitea.db
LOG_SQL = false

[repository]
ROOT = /var/lib/gitea/data/gitea-repositories

[server]
SSH_DOMAIN = my-server.mydomain.com
DOMAIN = my-server.mydomain.com
HTTP_PORT = 3000
APP_DATA_PATH = /var/lib/gitea/data
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = <my secret>
OFFLINE_MODE = false
ROOT_URL = http://localhost:3000/
PREFIX_URL = /gitea


[lfs]
PATH = /var/lib/gitea/data/lfs



[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost
ENABLE_RECAPTCHA = false
ENABLE_PASSWORD_RECOVERY = true

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[cron.update_checker]
ENABLED = false

[session]
PROVIDER = file

[log]
MODE = console
LEVEL = info
ROOT_PATH = /var/lib/gitea/log

[repository.pull-request]
DEFAULT_MERGE_STYLE = merge

[repository.signing]
DEFAULT_TRUST_MODEL = committer

[security]
INSTALL_LOCK = true
INTERNAL_TOKEN = <my token>
PASSWORD_HASH_ALGO = pbkdf2

[oauth2]
JWT_SECRET = <My secret>