# Gitea for Version Control

Hey, Good to see you around here! 👋😊

🧵 [This is the part 06 of the Home Lab 2.0 series.](https://fewmorewords.com/series/home-lab)

It took me almost 8 months to write this post 😬. Mostly I feel like these posts are increasingly becoming irrelevant. Hear me out.

We are at a critical point as a society in how we consume information on the web. Major search engines, especially `Google` are increasingly pushing AI-generated summaries as the primary response to search queries. From there, we are encouraged (🦾 strongly) to continue interacting with the AI through follow-up questions. Over time, this could reduce our incentive to visit independent blogs and websites altogether. I'm acutely aware of the change in my own behaviour in this context.

I'm not against AI (heck, you could say they're our carbon-neutral counterparts 👨🤖), but I disagree with the approach taken by big tech companies. They’re forcing these tools onto us while effectively turning all of us into their beta-testers.

That said, I decided to keep writing, at least for my own sake 🧘‍♂️.

My initial plan was to use `GitLab` as the version control system. However after reading about `Gitea`, I decided to give it a try. It is very lightweight and has many comparable features. Most importantly it is super easy to run on an LXC container.

I tried to stick to the plan of deploying the infra ( in this case the LXC ) using Terraform and then configuration using Ansible. Since I already have the foundation built for the rest of the home-lab this was relatively straight forward.

However I spent some time figuring out the curly issues related to `Keycloak` `SSO` integration. `Claude` was quite useful in this regard. `Gemini` was off the mark in many ways and ChatGPT was... well.. I don't really know.

[As alwasys, the code is in the repo. Check it out.](https://github.com/jayanath/home-lab/tree/main/gitea)

## Keycloak SSO Red herrings 🔴🐟

**1.** `kcadm.sh` **silently drops protocol mapper** `config` **in Keycloak 26.x**

When creating protocol mappers using `kcadm.sh create protocol-mappers`, the `config` block is silently ignored regardless of whether you pass it as heredoc JSON, `-s` flags, or `-f file`. The mapper is created but with an empty `config: {}`, meaning the groups claim is never included in tokens. Thanks Keycloak!

**Fix:** Use the Keycloak REST API directly via `ansible.builtin.uri` instead of `kcadm.sh` for any mapper creation tasks. See [`keycloak-gitea-integration.yml`](https://github.com/jayanath/home-lab/blob/main/keycloak/ansible/keycloak-gitea-integration.yml) and [`keycloak-vault-integration.yml`](https://github.com/jayanath/home-lab/blob/main/keycloak/ansible/keycloak-vault-integration.yml) for the working pattern.

**2.** `emailVerified` **defaults to** `false` **— blocks Gitea auto-registration**

When creating users via `kcadm.sh`, `emailVerified` defaults to `false` unless explicitly set. Gitea checks this field and will silently refuse to auto-create the account, routing the user to the `link_account` page instead with a generic "Registration is disabled" error.

**Fix:** Always set `"emailVerified": true` in all user creation and update tasks.

**3.** `ALLOW_ONLY_EXTERNAL_REGISTRATION` **alone is not enough for auto-registration**

Setting `ALLOW_ONLY_EXTERNAL_REGISTRATION = true` in `[service]` controls *who* can register (external OAuth users only), but does **not** actually trigger auto-account creation on first OAuth login. Without the `[oauth2_client]` section, Gitea routes every new OAuth user to the `link_account` page with "Registration is disabled" — even though the setting is correct.

**Fix:** Add this section to [`app.ini`](https://github.com/jayanath/home-lab/blob/main/gitea/ansible/roles/gitea/templates/app.ini.j2#L53):

```ini
[oauth2_client]
ENABLE_AUTO_REGISTRATION = true
ACCOUNT_LINKING          = auto
USERNAME                 = preferred_username
REGISTER_EMAIL_CONFIRM   = false
```

**4.** `--scopes` **flag in** `gitea admin auth add-oauth` **only takes the first word**

When passing `--scopes "openid profile email"` as a quoted string to the Gitea CLI, only `openid` is stored. The Gitea CLI splits on spaces and treats `profile` and `email` as separate unrecognised arguments without erroring.

**Fix:** Use a variable: `gitea_oauth2_scopes: "openid profile email"` and reference it in the command. Alternatively, verify the scopes in the DB after deployment:

```sql
SELECT cfg::jsonb->'Scopes' FROM login_source WHERE name = 'keycloak';
```

Expected: `["openid", "profile", "email"]`

**5\. Gitea OAuth2 source** `update-oauth` **does not reset missing fields**

If `--scopes` was missing when the source was first created, running `update-oauth` without `--scopes` will leave the old value intact. Always include all flags on both `add-oauth` and `update-oauth` commands.

**6\. Always verify token claims independently of the application**

When OAuth/OIDC login is failing silently, decode the token directly to verify what claims are actually being sent. Enable `directAccessGrantsEnabled` temporarily on the Keycloak client, get a token via curl, and decode the JWT payload:

```bash
curl -s -X POST http://localhost:8080/realms/homelab/protocol/openid-connect/token \
  -d "client_id=gitea&client_secret=<secret>&username=<user>&password=<pass>&grant_type=password" \
  | python3 -c "
import sys,json,base64
d=json.load(sys.stdin)
p=d['access_token'].split('.')[1]
p+='='*(4-len(p)%4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(p)),indent=2))
"
```

⚠️ Remember to disable direct grants again after testing.

## Finally 🎉

![](https://cdn.hashnode.com/uploads/covers/61834993fd5d634d0169641e/48945810-d04b-4738-901f-b61958b2a754.gif align="center")

## Reflection 🤔

At this point, I’ve stopped busting my rear trying to figure out code by combing through forums, blog posts, and product documentation. As I mentioned at the start of this post, I’m finding AI (especially Claude) to be a useful tool. It can generate code, and while it’s far from perfect, with enough patience and some trial-and-error cycles, I can usually get it into a working state. At least, that has been my experience so far.

I’m still not really sure whether this approach is actually productive, as I end up spending a lot of time fixing and testing things anyway 🙄. That said, one thing is clear: something feels missing when I rely on AI to generate large amounts of code. Maybe it’s nostalgia, or maybe it’s the satisfaction that came from digging through documentation, forums, and blog posts to solve problems manually. It's like fixing something around the house by myself vs hiring a contractor. Maybe..

I honestly don’t know where this all leads 🎭.

Thank you for sticking this far, I will see you (if you are a human) soon with another episode of this Home Lab series.

[Have a look at the human+AI collaborated code for this deployment](https://github.com/jayanath/home-lab) in my repo. Who knows, you may find a thing or two that may be useful 😅

## Reference 📚

[Gitea documentation. It is seriously good!](https://docs.gitea.com/?_gl=1*ri34r7*_gcl_au*MzkxMzUzNDExLjE3NzY0MTg2OTk.)
