Compare commits
No commits in common. "ab7fdd50d72ef59ff24bb48abb6bd583d20d5c54" and "47fbd4bb11b7574ebaf700871226ce542acc5a5c" have entirely different histories.
ab7fdd50d7
...
47fbd4bb11
35
.env.example
35
.env.example
@ -1,35 +0,0 @@
|
||||
# Copy to .env (./kit.sh init does it for you) and edit. .env is git-ignored.
|
||||
|
||||
# How to run: "docker" (docker compose, official image) or "binary" (official binary, no Docker).
|
||||
KIT_MODE=docker
|
||||
|
||||
APP_NAME="My Gitea"
|
||||
# Public hostname and URL (what people type in the browser). Trailing slash required.
|
||||
DOMAIN=localhost
|
||||
ROOT_URL=http://localhost:3000/
|
||||
HTTP_PORT=3000
|
||||
# Docker: host interface the port is published on. 127.0.0.1 = only reachable through your proxy/tunnel.
|
||||
BIND_ADDR=127.0.0.1
|
||||
# Set to true when ROOT_URL is https.
|
||||
COOKIE_SECURE=false
|
||||
|
||||
# Sign-up policy
|
||||
DISABLE_REGISTRATION=false
|
||||
MAX_REPOS_PER_USER=10
|
||||
REGISTER_EMAIL_CONFIRM=false
|
||||
MAILER_ENABLED=false
|
||||
|
||||
# First admin (created by ./kit.sh setup). Leave ADMIN_PASSWORD empty to generate one
|
||||
# (saved to secrets/admin.password, mode 0600).
|
||||
ADMIN_USER=admin
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=
|
||||
|
||||
# Default branch protection applied by ./kit.sh protect / protect-all.
|
||||
PROTECT_BRANCH=main
|
||||
# Approvals needed to merge a PR (0 lets a solo admin merge their own PRs; 1+ for teams).
|
||||
REQUIRED_APPROVALS=0
|
||||
|
||||
# Binary mode only: where Gitea keeps its data and config.
|
||||
GITEA_HOME=/var/lib/gitea
|
||||
GITEA_CONF=/etc/gitea/app.ini
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
.env
|
||||
config/app.ini
|
||||
data/
|
||||
secrets/
|
||||
bin/
|
||||
backups/
|
||||
*.zip
|
||||
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 The gitea-selfhost contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
178
README.md
178
README.md
@ -1,179 +1,3 @@
|
||||
# gitea-selfhost
|
||||
|
||||
Run your own [Gitea](https://about.gitea.com/) code host, set up the way the new internet's
|
||||
hosted Gitea is: public repos browsable without an account, open sign-up behind a captcha,
|
||||
`main` protected so every change lands through a pull request, and one Gitea user plus one
|
||||
scoped token per agent.
|
||||
|
||||
Two ways to run it, same config:
|
||||
|
||||
| | Docker (default) | No Docker |
|
||||
|---|---|---|
|
||||
| What runs | official image `gitea/gitea:28.1.0-rootless` | official binary 28.1.0 (sha256-verified download) |
|
||||
| Start | `./kit.sh up` | `KIT_MODE=binary` in `.env`, then `./kit.sh up` (or a systemd service) |
|
||||
| Data | `./data` | `GITEA_HOME` (default `/var/lib/gitea`) |
|
||||
| Config | `./config/app.ini` | `GITEA_CONF` (default `/etc/gitea/app.ini`) |
|
||||
|
||||
## One-command start
|
||||
|
||||
```sh
|
||||
git clone <this repo> gitea-selfhost && cd gitea-selfhost
|
||||
./kit.sh up
|
||||
```
|
||||
|
||||
The first run creates `.env` from `.env.example`. Edit it (at least `DOMAIN` and `ROOT_URL`), then
|
||||
run `./kit.sh up` again. It will:
|
||||
|
||||
1. render `config/app.ini` from `config/app.ini.template` with fresh random secrets (generated on
|
||||
your machine, never committed);
|
||||
2. start Gitea (Docker, or the downloaded binary) and wait for `/api/healthz`;
|
||||
3. create the admin user (password generated into `secrets/admin.password` unless you set
|
||||
`ADMIN_PASSWORD`) and an admin API token in `secrets/admin.token` (mode 0600).
|
||||
|
||||
Then open `ROOT_URL`. Needs: `bash`, `curl`, `python3` (for `protect-all`), and Docker with the
|
||||
compose plugin (Docker mode).
|
||||
|
||||
### No Docker: binary + systemd
|
||||
|
||||
```sh
|
||||
sed -i 's/^KIT_MODE=.*/KIT_MODE=binary/' .env
|
||||
sudo ./kit.sh install-service
|
||||
```
|
||||
|
||||
As root, that downloads and verifies the binary, creates a `git` system user, installs
|
||||
`/usr/local/bin/gitea`, renders the config into `GITEA_CONF`, installs and starts
|
||||
`systemd/gitea.service`, and creates the admin + token. Run later kit commands with `sudo`.
|
||||
|
||||
Without systemd, `./kit.sh up` in binary mode runs Gitea in the background with a pid file under
|
||||
`GITEA_HOME` (set `GITEA_HOME` / `GITEA_CONF` in `.env` to paths you can write).
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
./kit.sh up init + start + admin setup
|
||||
./kit.sh protect OWNER/REPO [BRANCH] block direct pushes to BRANCH (default main); PRs only
|
||||
./kit.sh protect-all protect the default branch of every non-empty repo
|
||||
./kit.sh agent NAME one Gitea user + one scoped token for an agent
|
||||
./kit.sh backup gitea dump into ./backups
|
||||
./kit.sh status | down
|
||||
```
|
||||
|
||||
## What the config does (`config/app.ini.template`)
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| `DISABLE_REGISTRATION` | `false` | anyone can sign up (set `true` in `.env` for invite-only) |
|
||||
| `ENABLE_CAPTCHA`, `CAPTCHA_TYPE` | `true`, `image` | built-in image captcha on sign-up, no third party |
|
||||
| `REGISTER_EMAIL_CONFIRM` | `false` | turn on only after `[mailer]` really delivers mail |
|
||||
| `REQUIRE_SIGNIN_VIEW` | `false` | public repos are browsable without an account |
|
||||
| `DEFAULT_ALLOW_CREATE_ORGANIZATION` | `false` | new users cannot create organizations |
|
||||
| `MAX_CREATION_LIMIT` | `10` | repos per regular user (admins are exempt) |
|
||||
| `DISABLE_MIGRATIONS`, `[mirror] DISABLE_NEW_PULL` | `true` | no importing or mirroring from arbitrary URLs |
|
||||
| `[security] EGRESS_MODE` | `lax` | webhooks may call public hosts only, not your LAN or loopback |
|
||||
| `DEFAULT_KEEP_EMAIL_PRIVATE` | `true` | new users' emails hidden by default |
|
||||
| `DEFAULT_BRANCH` | `main` | |
|
||||
| `DISABLE_SSH` | `true` | Git over HTTPS with a token; nothing to expose but one HTTP port |
|
||||
| `SSH_CREATE_AUTHORIZED_KEYS_FILE` | `false` | Gitea never touches the host user's `~/.ssh` |
|
||||
|
||||
Gitea has no instance-wide "protect main in every new repo" switch, so the kit applies the rule
|
||||
per repo: `./kit.sh protect OWNER/REPO`, or `./kit.sh protect-all` from a timer
|
||||
(`systemd/gitea-protect.timer` runs it every 5 minutes; with cron:
|
||||
`*/5 * * * * cd /opt/gitea-selfhost && ./kit.sh protect-all`). The rule blocks direct pushes for
|
||||
everyone, admins included; changes land through a PR. `REQUIRED_APPROVALS` in `.env` sets how many
|
||||
approvals a PR needs (0 lets a solo admin merge their own PR; Gitea never lets you approve your own).
|
||||
|
||||
## Agents and tokens
|
||||
|
||||
- One Gitea user per agent: `./kit.sh agent my-agent`. The token (in `secrets/agents/my-agent.token`)
|
||||
has `write:repository`, `write:issue` and `write:user`: it can create its own repos, push branches and
|
||||
open PRs, but has no admin scope.
|
||||
- Git remote: `https://<your host>/<owner>/<repo>.git`, username = agent name, password = token.
|
||||
Keep it out of URLs and shell history: `git config credential.helper store` with a 0600 file, or
|
||||
`GIT_ASKPASS`.
|
||||
- API: `Authorization: token <token>`.
|
||||
- Give an agent access to someone else's repo by adding it as a collaborator (write), not by sharing
|
||||
tokens. Never hand out the admin token. Rotate a token by generating a new one and deleting the old
|
||||
one in the user's settings (Applications).
|
||||
|
||||
## Put it behind a tunnel or reverse proxy
|
||||
|
||||
Keep Gitea on loopback (`BIND_ADDR=127.0.0.1`) and publish it through a proxy that terminates TLS.
|
||||
Set `ROOT_URL` to the public URL and `COOKIE_SECURE=true` once it is https, then restart.
|
||||
|
||||
**Caddy** (automatic HTTPS):
|
||||
|
||||
```
|
||||
git.example.com {
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
```
|
||||
|
||||
**nginx**:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name git.example.com;
|
||||
client_max_body_size 512m;
|
||||
location / {
|
||||
proxy_pass /gitea;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Under a sub-path (`https://example.com/git/`), set `ROOT_URL = https://example.com/git/` and strip
|
||||
the prefix in the proxy (`location /git/ { proxy_pass /gitea/; ... }`).
|
||||
|
||||
**Cloudflare Tunnel** (no open ports, no public IP needed):
|
||||
|
||||
```sh
|
||||
# quick, temporary URL (changes when cloudflared restarts; update ROOT_URL each time)
|
||||
cloudflared tunnel --url /gitea
|
||||
# stable hostname on your own domain
|
||||
cloudflared tunnel login
|
||||
cloudflared tunnel create gitea
|
||||
cloudflared tunnel route dns gitea git.example.com
|
||||
cloudflared tunnel run --url /gitea gitea
|
||||
```
|
||||
|
||||
## Backups
|
||||
|
||||
```sh
|
||||
./kit.sh backup # backups/gitea-dump-YYYYmmdd-HHMMSS.zip: repos, database, config
|
||||
```
|
||||
|
||||
The dump contains your `app.ini` secrets and every repo: store it encrypted and off the machine.
|
||||
Also keep a copy of `.env` and `secrets/`. Restore = stop Gitea, unzip the dump, put the repos,
|
||||
`gitea.db` and `app.ini` back in place (see the upstream
|
||||
[backup and restore guide](https://docs.gitea.com/administration/backup-and-restore)), start Gitea.
|
||||
|
||||
## Upgrades
|
||||
|
||||
1. `./kit.sh backup`.
|
||||
2. Read the [release notes](https://blog.gitea.com/) for breaking config changes.
|
||||
3. Docker: change the image tag in `docker-compose.yml`, then `docker compose pull && docker compose up -d`.
|
||||
Binary: change `GITEA_VERSION` in `kit.sh`, `rm bin/gitea && ./kit.sh up`
|
||||
(systemd: `sudo install -m 0755 bin/gitea /usr/local/bin/gitea && sudo systemctl restart gitea`).
|
||||
4. Gitea migrates the database on start. Check `./kit.sh status` and the log.
|
||||
|
||||
## Test the kit
|
||||
|
||||
```sh
|
||||
tests/e2e-binary.sh [PORT]
|
||||
```
|
||||
|
||||
Runs the whole flow in a throwaway directory on a spare port (default 3990): download and verify the
|
||||
binary, start, create the admin, create a repo, protect `main`, check that a direct push to `main` is
|
||||
rejected (even for the admin) while a PR merges, create an agent token (own repo yes, admin API no),
|
||||
check the new-user defaults (no org creation, 10-repo limit), migrations off, captcha on sign-up,
|
||||
backup. Then it stops Gitea and deletes everything.
|
||||
|
||||
## Licenses
|
||||
|
||||
- **Gitea** is free software under the MIT License, copyright the Gitea authors
|
||||
(<https://github.com/go-gitea/gitea>). This kit does not include or modify Gitea: it downloads the
|
||||
official binary or image.
|
||||
- **This kit** (scripts, templates, docs) is MIT-licensed too; see [LICENSE](LICENSE).
|
||||
Self-host kit: run your own Gitea with PR-only main, open sign-up behind a captcha, and per-agent tokens. Docker or binary + systemd.
|
||||
@ -1,99 +0,0 @@
|
||||
; Gitea app.ini template for gitea-selfhost.
|
||||
; Rendered by ./kit.sh init into config/app.ini with fresh random secrets.
|
||||
; Placeholders (dollar + braces) are filled from .env (see .env.example). Do not commit the rendered file.
|
||||
|
||||
APP_NAME = ${APP_NAME}
|
||||
RUN_MODE = prod
|
||||
WORK_PATH = ${WORK_PATH}
|
||||
|
||||
[server]
|
||||
PROTOCOL = http
|
||||
DOMAIN = ${DOMAIN}
|
||||
HTTP_ADDR = ${HTTP_ADDR}
|
||||
HTTP_PORT = ${HTTP_PORT}
|
||||
; The public URL people type in the browser (behind your tunnel / reverse proxy).
|
||||
ROOT_URL = ${ROOT_URL}
|
||||
; How Gitea reaches itself (hooks, internal API). Always loopback.
|
||||
LOCAL_ROOT_URL = http://127.0.0.1:${HTTP_PORT}/
|
||||
APP_DATA_PATH = ${WORK_PATH}/data
|
||||
; HTTP(S) only: Git over smart HTTP with a username + access token.
|
||||
DISABLE_SSH = true
|
||||
START_SSH_SERVER = false
|
||||
; Never touch the host user's ~/.ssh/authorized_keys.
|
||||
SSH_CREATE_AUTHORIZED_KEYS_FILE = false
|
||||
OFFLINE_MODE = true
|
||||
LFS_START_SERVER = false
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = ${WORK_PATH}/data/gitea.db
|
||||
|
||||
[repository]
|
||||
ROOT = ${WORK_PATH}/git/repositories
|
||||
DEFAULT_BRANCH = main
|
||||
DEFAULT_PRIVATE = public
|
||||
; Max repos per regular user (-1 = unlimited). Admins are exempt.
|
||||
MAX_CREATION_LIMIT = ${MAX_REPOS_PER_USER}
|
||||
; No importing / mirroring from arbitrary URLs.
|
||||
DISABLE_MIGRATIONS = true
|
||||
ENABLE_PUSH_CREATE_USER = false
|
||||
ENABLE_PUSH_CREATE_ORG = false
|
||||
|
||||
[mirror]
|
||||
; No new pull mirrors either.
|
||||
DISABLE_NEW_PULL = true
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY = ${SECRET_KEY}
|
||||
INTERNAL_TOKEN = ${INTERNAL_TOKEN}
|
||||
MIN_PASSWORD_LENGTH = 10
|
||||
PASSWORD_CHECK_PWN = false
|
||||
; Outgoing requests (webhooks, OAuth2 clients) may reach public hosts only; loopback / LAN
|
||||
; targets need an explicit [security] ALLOWED_HOST_LIST entry. Blocks SSRF into your network.
|
||||
EGRESS_MODE = lax
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = ${JWT_SECRET}
|
||||
|
||||
[service]
|
||||
; Public self-registration (set DISABLE_REGISTRATION=true in .env for invite-only).
|
||||
DISABLE_REGISTRATION = ${DISABLE_REGISTRATION}
|
||||
SHOW_REGISTRATION_BUTTON = true
|
||||
ENABLE_CAPTCHA = true
|
||||
CAPTCHA_TYPE = image
|
||||
REQUIRE_CAPTCHA_FOR_LOGIN = false
|
||||
; Only turn this on once [mailer] really delivers mail.
|
||||
REGISTER_EMAIL_CONFIRM = ${REGISTER_EMAIL_CONFIRM}
|
||||
REGISTER_MANUAL_CONFIRM = false
|
||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||
; Public repos stay browsable without an account.
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
|
||||
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
||||
DEFAULT_ENABLE_TIMINGS = false
|
||||
ENABLE_NOTIFY_MAIL = false
|
||||
NO_REPLY_ADDRESS = noreply.${DOMAIN}
|
||||
ENABLE_TIMELINE = true
|
||||
ENABLE_USER_HEATMAP = false
|
||||
AUTO_WATCH_NEW_REPOS = false
|
||||
AUTO_WATCH_ON_CHANGES = false
|
||||
|
||||
[mailer]
|
||||
ENABLED = ${MAILER_ENABLED}
|
||||
|
||||
[openid]
|
||||
ENABLE_OPENID_SIGNIN = false
|
||||
ENABLE_OPENID_SIGNUP = false
|
||||
|
||||
[session]
|
||||
PROVIDER = file
|
||||
COOKIE_SECURE = ${COOKIE_SECURE}
|
||||
|
||||
[log]
|
||||
MODE = console
|
||||
LEVEL = Info
|
||||
|
||||
[other]
|
||||
SHOW_FOOTER_VERSION = false
|
||||
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
|
||||
@ -1,15 +0,0 @@
|
||||
# gitea-selfhost: official Gitea image, pinned. Start with ./kit.sh up (it renders config first).
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:28.1.0-rootless
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
user: "${KIT_UID:-1000}:${KIT_GID:-1000}"
|
||||
environment:
|
||||
- GITEA_APP_INI=/etc/gitea/app.ini
|
||||
- GITEA_WORK_DIR=/var/lib/gitea
|
||||
volumes:
|
||||
- ./data:/var/lib/gitea
|
||||
- ./config:/etc/gitea
|
||||
ports:
|
||||
- "${BIND_ADDR:-127.0.0.1}:${HTTP_PORT:-3000}:${HTTP_PORT:-3000}"
|
||||
221
kit.sh
221
kit.sh
@ -1,221 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gitea-selfhost: run your own Gitea with PR-only main branches and per-agent tokens.
|
||||
# Usage: ./kit.sh up | init | setup | protect OWNER/REPO [BRANCH] | protect-all | agent NAME
|
||||
# | backup | upgrade | status | down | install-service
|
||||
set -euo pipefail
|
||||
|
||||
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$KIT_DIR"
|
||||
|
||||
GITEA_VERSION="28.1.0" # keep in step with docker-compose.yml
|
||||
SECRETS="$KIT_DIR/secrets"
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
log() { echo "==> $*"; }
|
||||
|
||||
load_env() {
|
||||
[ -f .env ] || { cp .env.example .env; log "created .env from .env.example (edit it, then re-run)"; }
|
||||
set -a; . ./.env; set +a
|
||||
KIT_MODE="${KIT_MODE:-docker}"
|
||||
HTTP_PORT="${HTTP_PORT:-3000}"
|
||||
PROTECT_BRANCH="${PROTECT_BRANCH:-main}"
|
||||
REQUIRED_APPROVALS="${REQUIRED_APPROVALS:-0}"
|
||||
case "$KIT_MODE" in
|
||||
docker) WORK_PATH=/var/lib/gitea; CONF_HOST="$KIT_DIR/config/app.ini" ;;
|
||||
binary) WORK_PATH="${GITEA_HOME:-/var/lib/gitea}"; CONF_HOST="${GITEA_CONF:-/etc/gitea/app.ini}" ;;
|
||||
*) die "KIT_MODE must be docker or binary" ;;
|
||||
esac
|
||||
API="http://127.0.0.1:${HTTP_PORT}/api/v1"
|
||||
export KIT_UID="$(id -u)" KIT_GID="$(id -g)" WORK_PATH HTTP_ADDR="${HTTP_ADDR:-$([ "$KIT_MODE" = docker ] && echo 0.0.0.0 || echo 127.0.0.1)}"
|
||||
}
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then docker-compose "$@"
|
||||
else die "docker compose not found (or set KIT_MODE=binary)"; fi
|
||||
}
|
||||
|
||||
bin_path() { if [ -x "$KIT_DIR/bin/gitea" ]; then echo "$KIT_DIR/bin/gitea"; else command -v gitea || echo /usr/local/bin/gitea; fi; }
|
||||
|
||||
# Run the gitea CLI against this instance.
|
||||
gitea_cli() {
|
||||
if [ "$KIT_MODE" = docker ]; then
|
||||
compose exec -T gitea gitea --config /etc/gitea/app.ini --work-path /var/lib/gitea "$@"
|
||||
elif [ "$(id -u)" = 0 ]; then
|
||||
# systemd install: Gitea refuses to run as root, so act as the service user.
|
||||
runuser -u git -- "$(bin_path)" --config "$CONF_HOST" --work-path "$WORK_PATH" "$@"
|
||||
else
|
||||
"$(bin_path)" --config "$CONF_HOST" --work-path "$WORK_PATH" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
rand() { head -c 64 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c "${1:-32}"; }
|
||||
|
||||
render_config() {
|
||||
# Fresh random secrets on first render; an existing config keeps its secrets.
|
||||
if [ -f "$CONF_HOST" ]; then log "config exists: $CONF_HOST (not overwritten)"; return; fi
|
||||
local s; s="$(cat config/app.ini.template)"
|
||||
SECRET_KEY="$(rand 64)"; INTERNAL_TOKEN="$(rand 105)"; JWT_SECRET="$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=')"
|
||||
local v
|
||||
for v in APP_NAME WORK_PATH DOMAIN HTTP_ADDR HTTP_PORT ROOT_URL MAX_REPOS_PER_USER DISABLE_REGISTRATION \
|
||||
REGISTER_EMAIL_CONFIRM MAILER_ENABLED COOKIE_SECURE SECRET_KEY INTERNAL_TOKEN JWT_SECRET; do
|
||||
s="${s//\$\{$v\}/${!v-}}"
|
||||
done
|
||||
case "$s" in *'${'*) die "unrendered placeholder in template: $(grep -o '\${[A-Z_]*}' <<<"$s" | head -1)";; esac
|
||||
mkdir -p "$(dirname "$CONF_HOST")"
|
||||
umask 077; printf '%s\n' "$s" > "$CONF_HOST"; umask 022
|
||||
log "wrote $CONF_HOST (secrets generated locally, never commit it)"
|
||||
}
|
||||
|
||||
install_binary() {
|
||||
[ -x "$KIT_DIR/bin/gitea" ] && "$KIT_DIR/bin/gitea" --version 2>/dev/null | grep -q " $GITEA_VERSION " && return
|
||||
local arch; case "$(uname -m)" in x86_64) arch=amd64;; aarch64|arm64) arch=arm64;; armv7l) arch=arm-6;; *) die "unsupported arch $(uname -m)";; esac
|
||||
local url="https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-${arch}"
|
||||
mkdir -p bin; log "downloading $url"
|
||||
curl -fsSL -o bin/gitea.tmp "$url"
|
||||
local want got; want="$(curl -fsSL "$url.sha256" | awk '{print $1}')"; got="$(sha256sum bin/gitea.tmp | awk '{print $1}')"
|
||||
[ -n "$want" ] && [ "$want" = "$got" ] || { rm -f bin/gitea.tmp; die "sha256 mismatch for gitea binary"; }
|
||||
chmod +x bin/gitea.tmp; mv bin/gitea.tmp bin/gitea; log "gitea $GITEA_VERSION verified (sha256 $got)"
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
local i; for i in $(seq 1 60); do
|
||||
curl -fsS "http://127.0.0.1:${HTTP_PORT}/api/healthz" >/dev/null 2>&1 && { log "gitea is up on 127.0.0.1:${HTTP_PORT}"; return; }
|
||||
sleep 1; done
|
||||
die "gitea did not become healthy on port ${HTTP_PORT}"
|
||||
}
|
||||
|
||||
cmd_init() {
|
||||
mkdir -p "$SECRETS"; chmod 700 "$SECRETS"
|
||||
if [ "$KIT_MODE" = docker ]; then mkdir -p data config; else install_binary; mkdir -p "$WORK_PATH"; fi
|
||||
render_config
|
||||
}
|
||||
|
||||
start() {
|
||||
if [ "$KIT_MODE" = docker ]; then compose up -d
|
||||
elif systemctl is-enabled gitea >/dev/null 2>&1; then sudo systemctl start gitea
|
||||
else
|
||||
mkdir -p "$WORK_PATH/log"
|
||||
if [ -f "$WORK_PATH/gitea.pid" ] && kill -0 "$(cat "$WORK_PATH/gitea.pid")" 2>/dev/null; then log "already running"; return; fi
|
||||
nohup "$(bin_path)" web --config "$CONF_HOST" --work-path "$WORK_PATH" >> "$WORK_PATH/log/gitea.out" 2>&1 < /dev/null &
|
||||
echo $! > "$WORK_PATH/gitea.pid"
|
||||
fi
|
||||
}
|
||||
|
||||
api() { # api METHOD PATH [JSON]
|
||||
local tok; tok="$(cat "$SECRETS/admin.token")"
|
||||
curl -fsS -X "$1" -H "Authorization: token $tok" -H "Content-Type: application/json" "$API$2" ${3:+-d "$3"}
|
||||
}
|
||||
|
||||
cmd_setup() {
|
||||
mkdir -p "$SECRETS"; chmod 700 "$SECRETS"
|
||||
local user="${ADMIN_USER:-admin}" pass="${ADMIN_PASSWORD:-}"
|
||||
if gitea_cli admin user list --admin 2>/dev/null | awk 'NR>1{print $2}' | grep -qx "$user"; then
|
||||
log "admin '$user' already exists"
|
||||
else
|
||||
if [ -z "$pass" ]; then pass="$(rand 24)"; (umask 077; printf '%s\n' "$pass" > "$SECRETS/admin.password"); fi
|
||||
gitea_cli admin user create --admin --username "$user" --password "$pass" \
|
||||
--email "${ADMIN_EMAIL:-admin@example.com}" --must-change-password=false >/dev/null
|
||||
log "created admin '$user' (password in secrets/admin.password if generated)"
|
||||
fi
|
||||
if [ ! -s "$SECRETS/admin.token" ]; then
|
||||
gitea_cli admin user generate-access-token --username "$user" --token-name "kit-admin-$(date +%s)" \
|
||||
--scopes "write:admin,write:repository,write:user,write:organization,write:issue" --raw \
|
||||
| tail -n1 | (umask 077; cat > "$SECRETS/admin.token")
|
||||
log "admin API token saved to secrets/admin.token (0600)"
|
||||
fi
|
||||
api GET /user >/dev/null && log "admin token works"
|
||||
}
|
||||
|
||||
cmd_protect() { # protect OWNER/REPO [BRANCH]
|
||||
local repo="$1" branch="${2:-$PROTECT_BRANCH}"
|
||||
local body; body=$(printf '{"rule_name":"%s","enable_push":false,"enable_merge_whitelist":false,"required_approvals":%s,"block_on_rejected_reviews":true,"dismiss_stale_approvals":true,"block_admin_merge_override":false}' "$branch" "$REQUIRED_APPROVALS")
|
||||
if api GET "/repos/$repo/branch_protections/$branch" >/dev/null 2>&1; then
|
||||
api PATCH "/repos/$repo/branch_protections/$branch" "$body" >/dev/null
|
||||
else
|
||||
api POST "/repos/$repo/branch_protections" "$body" >/dev/null
|
||||
fi
|
||||
echo "protected $repo:$branch (no direct pushes; merge through a PR; approvals=$REQUIRED_APPROVALS)"
|
||||
}
|
||||
|
||||
cmd_protect_all() {
|
||||
local page=1 names
|
||||
while :; do
|
||||
names=$(api GET "/repos/search?limit=50&page=$page" | python3 -c 'import json,sys; [print(r["full_name"], r["default_branch"] or "main", r["empty"]) for r in json.load(sys.stdin)["data"]]')
|
||||
[ -n "$names" ] || break
|
||||
while read -r full branch empty; do
|
||||
[ "$empty" = "True" ] && { echo "skip $full (empty)"; continue; }
|
||||
cmd_protect "$full" "$branch"
|
||||
done <<<"$names"
|
||||
page=$((page+1))
|
||||
done
|
||||
}
|
||||
|
||||
cmd_agent() { # agent NAME : one Gitea user + one scoped token per agent
|
||||
local name="$1"; [[ "$name" =~ ^[a-z0-9][a-z0-9-]{1,38}$ ]] || die "agent name: lowercase letters, digits, dashes"
|
||||
mkdir -p "$SECRETS/agents"; chmod 700 "$SECRETS/agents"
|
||||
if ! gitea_cli admin user list 2>/dev/null | awk 'NR>1{print $2}' | grep -qx "$name"; then
|
||||
gitea_cli admin user create --username "$name" --password "$(rand 32)" --email "$name@agents.invalid" \
|
||||
--must-change-password=false >/dev/null
|
||||
fi
|
||||
gitea_cli admin user generate-access-token --username "$name" --token-name "agent-$(date +%s)" \
|
||||
--scopes "write:repository,write:issue,write:user" --raw | tail -n1 | (umask 077; cat > "$SECRETS/agents/$name.token")
|
||||
echo "agent '$name' ready: token in secrets/agents/$name.token (scopes: write:repository, write:issue, write:user; no admin)"
|
||||
echo "git remote: ${ROOT_URL%/}/<owner>/<repo>.git (username $name, token as password)"
|
||||
}
|
||||
|
||||
cmd_backup() {
|
||||
mkdir -p backups
|
||||
local f="gitea-dump-$(date +%Y%m%d-%H%M%S).zip"
|
||||
if [ "$KIT_MODE" = docker ]; then
|
||||
compose exec -T gitea sh -c "cd /tmp && gitea dump --config /etc/gitea/app.ini --work-path /var/lib/gitea --file /tmp/$f >/dev/null && cat /tmp/$f && rm -f /tmp/$f" > "backups/$f"
|
||||
else
|
||||
gitea_cli dump --file "$KIT_DIR/backups/$f" >/dev/null
|
||||
fi
|
||||
chmod 600 "backups/$f"; log "backup: backups/$f (contains secrets; store it encrypted)"
|
||||
}
|
||||
|
||||
cmd_down() {
|
||||
if [ "$KIT_MODE" = docker ]; then compose down
|
||||
elif systemctl is-active gitea >/dev/null 2>&1; then sudo systemctl stop gitea
|
||||
elif [ -f "$WORK_PATH/gitea.pid" ]; then kill "$(cat "$WORK_PATH/gitea.pid")" 2>/dev/null || true; rm -f "$WORK_PATH/gitea.pid"; log "stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
curl -fsS "http://127.0.0.1:${HTTP_PORT}/api/v1/version" && echo || echo "not reachable on 127.0.0.1:${HTTP_PORT}"
|
||||
}
|
||||
|
||||
cmd_install_service() { # binary mode, as root: git user, binary, config, systemd unit, admin
|
||||
[ "$KIT_MODE" = binary ] || die "install-service is for KIT_MODE=binary"
|
||||
[ "$(id -u)" = 0 ] || die "run with sudo"
|
||||
install_binary
|
||||
id git >/dev/null 2>&1 || useradd --system --create-home --home-dir /home/git --shell /bin/bash git
|
||||
install -m 0755 "$KIT_DIR/bin/gitea" /usr/local/bin/gitea
|
||||
mkdir -p "$WORK_PATH" "$(dirname "$CONF_HOST")" "$SECRETS"; chmod 700 "$SECRETS"
|
||||
render_config
|
||||
chown -R git:git "$WORK_PATH" "$(dirname "$CONF_HOST")"; chmod 750 "$(dirname "$CONF_HOST")"; chmod 640 "$CONF_HOST"
|
||||
sed -e "s#/var/lib/gitea#$WORK_PATH#g" -e "s#/etc/gitea/app.ini#$CONF_HOST#g" -e "s#/etc/gitea#$(dirname "$CONF_HOST")#g" \
|
||||
systemd/gitea.service > /etc/systemd/system/gitea.service
|
||||
systemctl daemon-reload; systemctl enable --now gitea
|
||||
wait_healthy; cmd_setup
|
||||
log "systemd service 'gitea' enabled; run later kit commands with sudo"
|
||||
}
|
||||
|
||||
load_env
|
||||
case "${1:-help}" in
|
||||
init) cmd_init ;;
|
||||
up) cmd_init; start; wait_healthy; cmd_setup
|
||||
log "open ${ROOT_URL} (admin: ${ADMIN_USER:-admin})"
|
||||
log "protect a repo: ./kit.sh protect OWNER/REPO (or protect-all)" ;;
|
||||
setup) cmd_setup ;;
|
||||
protect) [ $# -ge 2 ] || die "usage: ./kit.sh protect OWNER/REPO [BRANCH]"; cmd_protect "$2" "${3:-}" ;;
|
||||
protect-all) cmd_protect_all ;;
|
||||
agent) [ $# -ge 2 ] || die "usage: ./kit.sh agent NAME"; cmd_agent "$2" ;;
|
||||
backup) cmd_backup ;;
|
||||
upgrade) die "edit GITEA_VERSION here and the image tag in docker-compose.yml, run ./kit.sh backup, then: docker compose pull && docker compose up -d (binary: rm bin/gitea && ./kit.sh up). See README." ;;
|
||||
status) cmd_status ;;
|
||||
down) cmd_down ;;
|
||||
install-service) cmd_install_service ;;
|
||||
*) sed -n '2,4p' "$0" ;;
|
||||
esac
|
||||
@ -1,8 +0,0 @@
|
||||
# Re-applies default branch protection to every repo (new repos included).
|
||||
[Unit]
|
||||
Description=gitea-selfhost: protect default branches
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/gitea-selfhost
|
||||
ExecStart=/opt/gitea-selfhost/kit.sh protect-all
|
||||
@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=gitea-selfhost: protect default branches every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@ -1,24 +0,0 @@
|
||||
# Binary mode: install with sudo ./kit.sh install-service
|
||||
# Assumes a "git" system user, binary at /usr/local/bin/gitea, data in /var/lib/gitea,
|
||||
# config in /etc/gitea/app.ini (the defaults in .env.example).
|
||||
[Unit]
|
||||
Description=Gitea (gitea-selfhost)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=git
|
||||
Group=git
|
||||
WorkingDirectory=/var/lib/gitea
|
||||
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini --work-path /var/lib/gitea
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
|
||||
# Hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/var/lib/gitea /etc/gitea
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# End-to-end check of the kit in binary mode, in a throwaway directory on a spare port.
|
||||
# Usage: tests/e2e-binary.sh [PORT] (default 3990). Leaves nothing behind.
|
||||
set -euo pipefail
|
||||
PORT="${1:-3990}"
|
||||
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TMP="$(mktemp -d)"; WORK="$TMP/kit"; CLONE="$TMP/clone"
|
||||
pass=0; fail=0
|
||||
ok() { echo "PASS $*"; pass=$((pass+1)); }
|
||||
bad() { echo "FAIL $*"; fail=$((fail+1)); }
|
||||
cleanup() { (cd "$WORK" 2>/dev/null && ./kit.sh down >/dev/null 2>&1) || true; rm -rf "$TMP"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
ss -ltn 2>/dev/null | grep -q ":$PORT " && { echo "port $PORT is busy"; exit 2; }
|
||||
mkdir -p "$WORK"; cp -r "$SRC"/. "$WORK"/; rm -rf "$WORK/.git" "$WORK/.env" "$WORK/secrets" "$WORK/data"
|
||||
cd "$WORK"
|
||||
cat > .env <<ENV
|
||||
KIT_MODE=binary
|
||||
APP_NAME="Kit E2E"
|
||||
DOMAIN=127.0.0.1
|
||||
ROOT_URL=http://127.0.0.1:$PORT/
|
||||
HTTP_PORT=$PORT
|
||||
COOKIE_SECURE=false
|
||||
DISABLE_REGISTRATION=false
|
||||
MAX_REPOS_PER_USER=10
|
||||
REGISTER_EMAIL_CONFIRM=false
|
||||
MAILER_ENABLED=false
|
||||
ADMIN_USER=kitadmin
|
||||
ADMIN_EMAIL=kitadmin@example.com
|
||||
ADMIN_PASSWORD=
|
||||
PROTECT_BRANCH=main
|
||||
REQUIRED_APPROVALS=0
|
||||
GITEA_HOME=$TMP/home
|
||||
GITEA_CONF=$TMP/home/conf/app.ini
|
||||
ENV
|
||||
./kit.sh up && ok "kit.sh up (download + sha256, render config, start, admin + token)" || { bad "kit.sh up"; exit 1; }
|
||||
|
||||
A="http://127.0.0.1:$PORT/api/v1"; T="$(cat secrets/admin.token)"; P="$(cat secrets/admin.password)"
|
||||
H=(-H "Authorization: token $T" -H "Content-Type: application/json")
|
||||
curl -fsS "${H[@]}" -X POST "$A/user/repos" -d '{"name":"demo","auto_init":true,"default_branch":"main"}' >/dev/null && ok "admin created repo kitadmin/demo" || bad "create repo"
|
||||
./kit.sh protect kitadmin/demo >/dev/null && ok "branch protection applied to main" || bad "protect"
|
||||
|
||||
git -c credential.helper= clone -q "http://kitadmin:$P@127.0.0.1:$PORT/kitadmin/demo.git" "$CLONE"
|
||||
cd "$CLONE"; git config user.name e2e; git config user.email e2e@example.com
|
||||
echo direct > direct.txt; git add direct.txt; git commit -qm direct
|
||||
if git -c credential.helper= push -q origin main 2>/dev/null; then bad "direct push to main was ACCEPTED"; else ok "direct push to main rejected (even for the admin)"; fi
|
||||
git reset -q --hard origin/main; git checkout -qb feature; echo via-pr > pr.txt; git add pr.txt; git commit -qm "via PR"
|
||||
git -c credential.helper= push -q origin feature 2>/dev/null && ok "push to a feature branch accepted" || bad "feature push"
|
||||
cd "$WORK"
|
||||
N=$(curl -fsS "${H[@]}" -X POST "$A/repos/kitadmin/demo/pulls" -d '{"head":"feature","base":"main","title":"via PR"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["number"])')
|
||||
curl -fsS "${H[@]}" -X POST "$A/repos/kitadmin/demo/pulls/$N/merge" -d '{"Do":"merge"}' >/dev/null && ok "PR #$N merged into main" || bad "merge"
|
||||
curl -fsS "${H[@]}" "$A/repos/kitadmin/demo/contents/pr.txt?ref=main" >/dev/null && ok "merged change is on main" || bad "merged file missing"
|
||||
|
||||
./kit.sh agent e2e-agent >/dev/null && ok "agent user + scoped token created" || bad "agent"
|
||||
AT="$(cat secrets/agents/e2e-agent.token)"
|
||||
curl -fsS -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/user/repos" -d '{"name":"agent-repo","auto_init":true}' >/dev/null && ok "agent token can create its own repo" || bad "agent create repo"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" "$A/admin/users"); [ "$code" = 403 ] && ok "agent token has no admin access (403)" || bad "agent admin access: $code"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "${H[@]}" -H "Sudo: e2e-agent" -X POST "$A/orgs" -d '{"username":"e2e-org"}'); [ "$code" = 403 ] && ok "new users cannot create organizations (403)" || bad "org creation by new user: $code"
|
||||
st=$(curl -fsS "${H[@]}" "$A/settings/repository"); grep -q '"migrations_disabled":true' <<<"$st" && ok "migrations disabled" || bad "migrations"
|
||||
page=$(curl -fsS "http://127.0.0.1:$PORT/user/sign_up"); grep -q 'captcha' <<<"$page" && ok "public sign-up page shows a captcha" || bad "sign-up captcha"
|
||||
./kit.sh backup >/dev/null 2>&1 && ls backups/*.zip >/dev/null && ok "backup (gitea dump) written" || bad "backup"
|
||||
./kit.sh protect-all >/dev/null && ok "protect-all ran" || bad "protect-all"
|
||||
|
||||
made=1; for i in $(seq 2 11); do
|
||||
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/user/repos" -d "{\"name\":\"r$i\"}")
|
||||
[ "$c" = 201 ] && made=$((made+1))
|
||||
done
|
||||
[ "$made" = 10 ] && ok "repo limit: regular user stops at 10 repos" || bad "repo limit: user made $made repos"
|
||||
if grep -q "Deprecation" "$TMP/home/log/gitea.out"; then bad "config uses deprecated keys: $(grep -o 'config option `[^`]*`' "$TMP/home/log/gitea.out" | sort -u | tr '\n' ' ')"; else ok "no deprecated config keys in the Gitea log"; fi
|
||||
|
||||
echo; echo "$pass passed, $fail failed"
|
||||
[ "$fail" = 0 ]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user