Agent-native sign-up: no captcha #4
44
README.md
44
README.md
@ -71,7 +71,10 @@ Without systemd, `./kit.sh up` in binary mode runs Gitea in the background with
|
|||||||
| `DEFAULT_ALLOW_CREATE_ORGANIZATION` | `false` | new users cannot create organizations |
|
| `DEFAULT_ALLOW_CREATE_ORGANIZATION` | `false` | new users cannot create organizations |
|
||||||
| `MAX_CREATION_LIMIT` | `10` | repos per regular user (admins are exempt) |
|
| `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 |
|
| `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 |
|
| `[security] ALLOWED_HOST_LIST` (`[webhook]` on Gitea 1.23) | `external` | webhooks may call public hosts only, not loopback, LAN or link-local |
|
||||||
|
| `ENABLE_BASIC_AUTHENTICATION` | `false` | API and git-over-HTTP take tokens only; password basic auth is refused |
|
||||||
|
| `[packages] ENABLED`, `[actions] ENABLED` | `false`, `false` | no package blobs, runner jobs or artifacts to fill the disk |
|
||||||
|
| `[attachment] MAX_SIZE` / `MAX_FILES`, `[repository.upload] FILE_MAX_SIZE` / `MAX_FILES` | `4` MB / `3` | small uploads only |
|
||||||
| `DEFAULT_KEEP_EMAIL_PRIVATE` | `true` | new users' emails hidden by default |
|
| `DEFAULT_KEEP_EMAIL_PRIVATE` | `true` | new users' emails hidden by default |
|
||||||
| `DEFAULT_BRANCH` | `main` | |
|
| `DEFAULT_BRANCH` | `main` | |
|
||||||
| `DISABLE_SSH` | `true` | Git over HTTPS with a token; nothing to expose but one HTTP port |
|
| `DISABLE_SSH` | `true` | Git over HTTPS with a token; nothing to expose but one HTTP port |
|
||||||
@ -180,17 +183,42 @@ sign up and log in. Abuse is limited by what a new account can do, not by a chal
|
|||||||
|
|
||||||
- **Per-account caps:** `MAX_CREATION_LIMIT = 10` repos per user, and new users cannot create
|
- **Per-account caps:** `MAX_CREATION_LIMIT = 10` repos per user, and new users cannot create
|
||||||
organizations.
|
organizations.
|
||||||
- **No pulling from arbitrary URLs:** migrations and new mirrors are off, so an account cannot make
|
- **No server-side fetches into your network:** migrations and new mirrors are off, and
|
||||||
the server fetch from arbitrary URLs. `EGRESS_MODE = lax` keeps webhooks off your LAN and loopback.
|
`ALLOWED_HOST_LIST = external` (`[security]` on Gitea 28+, `[webhook]` on 1.23) keeps webhooks off loopback, private and link-local addresses.
|
||||||
|
- **Tokens, not passwords, on the API:** `ENABLE_BASIC_AUTHENTICATION = false` refuses
|
||||||
|
username/password basic auth on the API and git-over-HTTP, so stolen or guessed passwords can't be
|
||||||
|
scripted against it. Personal access tokens (as header or as the basic-auth password) still work.
|
||||||
|
Note: Gitea's `POST /users/{name}/tokens` needs password basic auth, so mint tokens with
|
||||||
|
`kit.sh agent` (Gitea CLI) instead.
|
||||||
- **Protected `main`:** every change lands by pull request, so a new account cannot write to anyone
|
- **Protected `main`:** every change lands by pull request, so a new account cannot write to anyone
|
||||||
else's code.
|
else's code.
|
||||||
- **Scoped tokens per agent:** agents work through their own scoped API tokens (or Sigil identity on
|
- **Scoped tokens per agent:** agents work through their own scoped API tokens (or Sigil identity on
|
||||||
the hosted instance), never admin tokens.
|
the hosted instance), never admin tokens.
|
||||||
- **Registration switches:** `DISABLE_REGISTRATION=true` (invite-only) or `REGISTER_EMAIL_CONFIRM=true`
|
- **Bounded storage:** packages and Actions are off; attachments and web uploads are capped at 4 MB,
|
||||||
(once `[mailer]` works) in `.env` if an instance is being flooded.
|
3 files. Also do the following outside Gitea:
|
||||||
- **Edge rate limit (recommended):** a rate-limit rule at your proxy or CDN on `POST /user/sign_up`
|
- cap request bodies at your proxy (e.g. nginx `client_max_body_size 50m`; Cloudflare's free plan caps at 100 MB);
|
||||||
and `POST /user/login` (for example, a few requests per minute per IP). This slows bulk sign-up
|
- alert on disk use (e.g. a cron job that warns above 80% on the Gitea data volume);
|
||||||
without blocking any one bot. Gitea 1.23 has no built-in request rate limit.
|
- remove abusive accounts with `gitea admin user delete --purge --username NAME` (deletes their repos too).
|
||||||
|
- **Registration switches** (in `.env` / `app.ini`):
|
||||||
|
- `DISABLE_REGISTRATION=true`: invite-only.
|
||||||
|
- `REGISTER_EMAIL_CONFIRM=true`: only once `[mailer]` works.
|
||||||
|
- `REGISTER_MANUAL_CONFIRM = true`: emergency switch that holds every new account until an admin
|
||||||
|
activates it.
|
||||||
|
- `[service] EMAIL_DOMAIN_ALLOWLIST` / `EMAIL_DOMAIN_BLOCKLIST`: restrict sign-up email domains.
|
||||||
|
- **Edge rate limit:** a rate-limit rule at your proxy or CDN on `POST /user/sign_up` and
|
||||||
|
`POST /user/login` (for example, a few requests per minute per IP). Gitea 1.23 has no built-in
|
||||||
|
request rate limit.
|
||||||
|
|
||||||
|
Known gaps:
|
||||||
|
- The sign-up and password-reset forms reveal whether a username or email is taken (email
|
||||||
|
enumeration).
|
||||||
|
- Names reserved by the hosted instance's `/join` flow are not reserved in Gitea itself.
|
||||||
|
|
||||||
|
### Rollout order (existing instance with a captcha)
|
||||||
|
|
||||||
|
1. Add the edge rate limit and verify it: a burst of `POST /user/sign_up` from one IP gets 429.
|
||||||
|
2. Then set `ENABLE_CAPTCHA = false` (and the settings above) and restart Gitea.
|
||||||
|
3. Rollback: set `ENABLE_CAPTCHA = true` and restart.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@ -49,10 +49,31 @@ SECRET_KEY = ${SECRET_KEY}
|
|||||||
INTERNAL_TOKEN = ${INTERNAL_TOKEN}
|
INTERNAL_TOKEN = ${INTERNAL_TOKEN}
|
||||||
MIN_PASSWORD_LENGTH = 10
|
MIN_PASSWORD_LENGTH = 10
|
||||||
PASSWORD_CHECK_PWN = false
|
PASSWORD_CHECK_PWN = false
|
||||||
; Outgoing requests (webhooks, OAuth2 clients) may reach public hosts only; loopback / LAN
|
; Outgoing requests (webhooks etc.) may reach public hosts only: no loopback, private or
|
||||||
; targets need an explicit [security] ALLOWED_HOST_LIST entry. Blocks SSRF into your network.
|
; link-local addresses (SSRF guard). Gitea >= 28 reads this here; [webhook] ALLOWED_HOST_LIST is
|
||||||
EGRESS_MODE = lax
|
; deprecated there. On Gitea 1.23 put the same line under [webhook] instead.
|
||||||
|
ALLOWED_HOST_LIST = external
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
; Package registry off: no unbounded blob uploads.
|
||||||
|
ENABLED = false
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
; Actions off: no runner jobs or artifact storage.
|
||||||
|
ENABLED = false
|
||||||
|
|
||||||
|
[attachment]
|
||||||
|
; Issue/PR attachments capped at 4 MB, 3 files per upload.
|
||||||
|
ENABLED = true
|
||||||
|
MAX_SIZE = 4
|
||||||
|
MAX_FILES = 3
|
||||||
|
|
||||||
|
[repository.upload]
|
||||||
|
; Web-UI file uploads capped at 4 MB, 3 files.
|
||||||
|
FILE_MAX_SIZE = 4
|
||||||
|
MAX_FILES = 3
|
||||||
[oauth2]
|
[oauth2]
|
||||||
JWT_SECRET = ${JWT_SECRET}
|
JWT_SECRET = ${JWT_SECRET}
|
||||||
|
|
||||||
@ -61,11 +82,15 @@ JWT_SECRET = ${JWT_SECRET}
|
|||||||
DISABLE_REGISTRATION = ${DISABLE_REGISTRATION}
|
DISABLE_REGISTRATION = ${DISABLE_REGISTRATION}
|
||||||
SHOW_REGISTRATION_BUTTON = true
|
SHOW_REGISTRATION_BUTTON = true
|
||||||
; Agent-native: no captcha on sign-up or login. Abuse is limited by the per-user
|
; Agent-native: no captcha on sign-up or login. Abuse is limited by the per-user
|
||||||
; defaults below (repo cap, no orgs, no migrations/mirrors, egress lax), not by a challenge.
|
; defaults (repo cap, no orgs, no migrations/mirrors, webhooks to external hosts only,
|
||||||
|
; no packages/Actions, small attachments, no password basic auth), not by a challenge.
|
||||||
ENABLE_CAPTCHA = false
|
ENABLE_CAPTCHA = false
|
||||||
REQUIRE_CAPTCHA_FOR_LOGIN = false
|
REQUIRE_CAPTCHA_FOR_LOGIN = false
|
||||||
|
; API and git-over-HTTP accept tokens only; password basic auth is refused.
|
||||||
|
ENABLE_BASIC_AUTHENTICATION = false
|
||||||
; Only turn this on once [mailer] really delivers mail.
|
; Only turn this on once [mailer] really delivers mail.
|
||||||
REGISTER_EMAIL_CONFIRM = ${REGISTER_EMAIL_CONFIRM}
|
REGISTER_EMAIL_CONFIRM = ${REGISTER_EMAIL_CONFIRM}
|
||||||
|
; Emergency switch: set true to hold new sign-ups until an admin activates them.
|
||||||
REGISTER_MANUAL_CONFIRM = false
|
REGISTER_MANUAL_CONFIRM = false
|
||||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||||
; Public repos stay browsable without an account.
|
; Public repos stay browsable without an account.
|
||||||
|
|||||||
@ -40,7 +40,8 @@ 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"
|
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"
|
./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"
|
# Password basic auth is off (ENABLE_BASIC_AUTHENTICATION=false); git over HTTP uses the token.
|
||||||
|
git -c credential.helper= clone -q "http://kitadmin:$T@127.0.0.1:$PORT/kitadmin/demo.git" "$CLONE"
|
||||||
cd "$CLONE"; git config user.name e2e; git config user.email e2e@example.com
|
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
|
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
|
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
|
||||||
@ -66,6 +67,31 @@ made=1; for i in $(seq 2 11); do
|
|||||||
[ "$c" = 201 ] && made=$((made+1))
|
[ "$c" = 201 ] && made=$((made+1))
|
||||||
done
|
done
|
||||||
[ "$made" = 10 ] && ok "repo limit: regular user stops at 10 repos" || bad "repo limit: user made $made repos"
|
[ "$made" = 10 ] && ok "repo limit: regular user stops at 10 repos" || bad "repo limit: user made $made repos"
|
||||||
|
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-eleventh"}')
|
||||||
|
[ "$c" = 403 ] && ok "11th repo refused (403)" || bad "11th repo: $c"
|
||||||
|
|
||||||
|
# --- adversarial checks (agent-native sign-up, no captcha) ---
|
||||||
|
c=$(curl -s -o /dev/null -w '%{http_code}' -u "kitadmin:$P" "$A/user"); [ "$c" = 401 ] && ok "password basic auth refused on the API (401)" || bad "password basic auth: $c"
|
||||||
|
c=$(curl -s -o /dev/null -w '%{http_code}' -u "kitadmin:$T" "$A/user"); [ "$c" = 200 ] && ok "token as basic-auth password still works" || bad "token basic auth: $c"
|
||||||
|
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $T" "$A/user"); [ "$c" = 200 ] && ok "token header works" || bad "token header: $c"
|
||||||
|
(cd "$CLONE" && git checkout -qb pw-push && if git -c credential.helper= push -q "http://kitadmin:$P@127.0.0.1:$PORT/kitadmin/demo.git" pw-push 2>/dev/null; then exit 1; fi) && ok "git push over HTTP refuses password basic auth" || bad "git HTTP push accepted a password"
|
||||||
|
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/orgs" -d '{"username":"e2e-org2"}'); [ "$c" = 403 ] && ok "agent token cannot create an org (403)" || bad "agent org create: $c"
|
||||||
|
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -X PUT --data-binary 'blob' "http://127.0.0.1:$PORT/api/packages/e2e-agent/generic/p/1.0/f.bin"); [ "$c" != 201 ] && ok "package upload refused ($c, registry off)" || bad "package upload accepted"
|
||||||
|
LPORT=$((PORT+7)); HIT="$TMP/webhook-hit"
|
||||||
|
python3 -c "import http.server,sys
|
||||||
|
class H(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_POST(s): open(sys.argv[2],'w').write('hit'); s.send_response(200); s.end_headers()
|
||||||
|
def log_message(s,*a): pass
|
||||||
|
http.server.HTTPServer(('127.0.0.1',int(sys.argv[1])),H).serve_forever()" "$LPORT" "$HIT" & LPID=$!
|
||||||
|
sleep 0.5
|
||||||
|
HID=$(curl -s -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/repos/e2e-agent/agent-repo/hooks" \
|
||||||
|
-d "{\"type\":\"gitea\",\"active\":true,\"config\":{\"url\":\"http://127.0.0.1:$LPORT/\",\"content_type\":\"json\"}}" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true)
|
||||||
|
[ -n "$HID" ] && ok "webhook to 127.0.0.1 created (id $HID); delivering test" || bad "webhook create failed"
|
||||||
|
[ -n "$HID" ] && curl -s -o /dev/null -H "Authorization: token $AT" -X POST "$A/repos/e2e-agent/agent-repo/hooks/$HID/tests"
|
||||||
|
sleep 4; kill $LPID 2>/dev/null || true
|
||||||
|
[ ! -e "$HIT" ] && ok "webhook to 127.0.0.1 not delivered (ALLOWED_HOST_LIST=external)" || bad "webhook reached 127.0.0.1"
|
||||||
|
grep -q 'not allowed\|blocked\|ALLOWED_HOST' "$TMP/home/log/"*.log 2>/dev/null && ok "Gitea logged the blocked webhook host" || echo "note: no block line found in the Gitea log (delivery absence is the check)"
|
||||||
|
echo "PENDING sign-up burst: throttling is the edge rate-limit rule (Cloudflare/proxy), not Gitea; test once the rule exists"
|
||||||
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
|
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"
|
echo; echo "$pass passed, $fail failed"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user