diff --git a/docs/gitea-ssh-migration-guide.md b/docs/gitea-ssh-migration-guide.md new file mode 100644 index 0000000..443bd62 --- /dev/null +++ b/docs/gitea-ssh-migration-guide.md @@ -0,0 +1,245 @@ +# Gitea SSH Migration Guide + +Guide for migrating between Gitea SSH modes and updating Git remote URLs. + +## SSH Modes Overview + +### Passthrough Mode (Default) +- **Port**: 22 (standard SSH) +- **URL Format**: `git@git.jnss.me:user/repo.git` +- **Security**: System fail2ban protects all SSH traffic +- **Recommended**: ✅ For production use + +### Dedicated Mode (Fallback) +- **Port**: 2222 (Gitea SSH server) +- **URL Format**: `ssh://git@git.jnss.me:2222/user/repo.git` +- **Security**: Separate fail2ban jail for port 2222 +- **Use Case**: Debugging or when passthrough has issues + +--- + +## Migration: Dedicated → Passthrough (Default) + +When you deploy the new code, Gitea will automatically switch to passthrough mode. + +### What Happens Automatically + +1. ✅ Gitea's SSH server stops listening on port 2222 +2. ✅ Port 2222 firewall rule removed +3. ✅ System SSH configured for Git passthrough +4. ✅ AuthorizedKeysCommand script deployed +5. ✅ fail2ban switches to system `sshd` jail + +### What You Need to Do + +**Update your Git remote URLs** in each repository: + +```bash +# Check current remote URL +git remote -v + +# Update to new format (no port number) +git remote set-url origin git@git.jnss.me:username/repo.git + +# Verify new URL +git remote -v + +# Test connection +git fetch +``` + +### Bulk Update Script + +If you have many repositories, use this script: + +```bash +#!/bin/bash +# migrate-git-urls.sh - Update all Git remotes from dedicated to passthrough + +# Find all git repositories in current directory and subdirectories +find . -type d -name '.git' | while read gitdir; do + repo=$(dirname "$gitdir") + echo "Processing: $repo" + + cd "$repo" + + # Get current origin URL + current_url=$(git remote get-url origin 2>/dev/null) + + # Check if it's the old format (with :2222) + if [[ $current_url == *":2222/"* ]]; then + # Convert to new format + new_url=$(echo "$current_url" | sed 's|ssh://git@git.jnss.me:2222/|git@git.jnss.me:|') + + echo " Old: $current_url" + echo " New: $new_url" + + git remote set-url origin "$new_url" + echo " ✅ Updated" + else + echo " ℹ️ Already using correct format or not Gitea" + fi + + cd - > /dev/null + echo "" +done + +echo "Migration complete!" +``` + +**Usage:** +```bash +chmod +x migrate-git-urls.sh +./migrate-git-urls.sh +``` + +--- + +## Migration: Passthrough → Dedicated + +If you need to switch back to dedicated mode: + +### 1. Update Configuration + +Edit `host_vars/arch-vps/main.yml`: +```yaml +gitea_ssh_mode: "dedicated" +``` + +### 2. Deploy + +```bash +ansible-playbook -i inventory/hosts.yml rick-infra.yml --limit arch-vps +``` + +### 3. Update Git Remotes + +```bash +# Update to dedicated format (with :2222 port) +git remote set-url origin ssh://git@git.jnss.me:2222/username/repo.git + +# Test connection +ssh -T -p 2222 git@git.jnss.me +git fetch +``` + +--- + +## URL Format Reference + +### Passthrough Mode (Port 22) +```bash +# Clone +git clone git@git.jnss.me:username/repo.git + +# Add remote +git remote add origin git@git.jnss.me:username/repo.git + +# SSH test +ssh -T git@git.jnss.me +``` + +### Dedicated Mode (Port 2222) +```bash +# Clone +git clone ssh://git@git.jnss.me:2222/username/repo.git + +# Add remote +git remote add origin ssh://git@git.jnss.me:2222/username/repo.git + +# SSH test +ssh -T -p 2222 git@git.jnss.me +``` + +--- + +## Troubleshooting + +### After Migration, Git Operations Fail + +**Symptom**: `git push` fails with "Permission denied" or "Connection refused" + +**Solution**: +1. Check your remote URL format: + ```bash + git remote -v + ``` + +2. Update if needed: + ```bash + # For passthrough (no port) + git remote set-url origin git@git.jnss.me:username/repo.git + + # For dedicated (with port) + git remote set-url origin ssh://git@git.jnss.me:2222/username/repo.git + ``` + +3. Test SSH connection: + ```bash + # Passthrough + ssh -T git@git.jnss.me + + # Dedicated + ssh -T -p 2222 git@git.jnss.me + ``` + +### SSH Key Not Recognized After Migration + +**Symptom**: "Permission denied (publickey)" + +**Cause**: SSH keys are stored in Gitea's database, not affected by mode change. + +**Solution**: +1. Verify your SSH key is in Gitea: + - Log into Gitea web interface + - Go to Settings → SSH/GPG Keys + - Check your key is listed + +2. Test key locally: + ```bash + ssh-add -l # List loaded keys + ``` + +3. Try with explicit key: + ```bash + ssh -T -i ~/.ssh/id_ed25519 git@git.jnss.me + ``` + +### Port 2222 Still Open After Switching to Passthrough + +**Symptom**: `nc -zv git.jnss.me 2222` succeeds + +**Cause**: Gitea service may still be running on port 2222 + +**Solution**: +```bash +# On the server +systemctl restart gitea +ss -tlnp | grep 2222 # Should show nothing +``` + +--- + +## Verification Checklist + +After migration, verify: + +- [ ] SSH connection works: `ssh -T git@git.jnss.me` (passthrough) or `ssh -T -p 2222 git@git.jnss.me` (dedicated) +- [ ] Can clone repository with new URL format +- [ ] Can push commits to repository +- [ ] fail2ban is active: `fail2ban-client status sshd` (passthrough) or `fail2ban-client status gitea-ssh` (dedicated) +- [ ] Firewall configured correctly: `nft list ruleset | grep 2222` (should show nothing in passthrough) + +--- + +## Notes + +- **Both modes are fully supported** - choose what works best for your setup +- **No data loss** - repositories, users, and SSH keys are unaffected by mode changes +- **Gradual migration** - you can update remote URLs at your own pace (old URLs may still work for a short time) +- **Team coordination** - if you're in a team, coordinate the migration so everyone updates their URLs + +--- + +**Rick-Infra Gitea SSH Migration Guide** +Switch between passthrough and dedicated SSH modes safely. diff --git a/docs/service-domain-configuration.md b/docs/service-domain-configuration.md new file mode 100644 index 0000000..c61f2b9 --- /dev/null +++ b/docs/service-domain-configuration.md @@ -0,0 +1,367 @@ +# Service Domain Configuration Standard + +Standard pattern for domain configuration in rick-infra service roles. + +## Architecture Philosophy + +Rick-infra follows a **direct domain specification** pattern for service configuration: + +```yaml +# Direct and explicit +service_domain: "subdomain.jnss.me" + +# NOT this (complex and inflexible) +service_subdomain: "subdomain" +service_domain: "{{ caddy_domain }}" +service_full_domain: "{{ service_subdomain }}.{{ service_domain }}" +``` + +## Benefits + +1. **Simplicity**: One variable instead of three +2. **Flexibility**: Can use any domain (subdomain, root, or completely different) +3. **Explicitness**: Clear what domain the service uses +4. **No Forced Inheritance**: Not tied to infrastructure `caddy_domain` +5. **Consistency**: All services follow the same pattern + +--- + +## Standard Pattern + +### Basic Service (Single Domain) + +For services that only need one domain: + +```yaml +# roles/service/defaults/main.yml +service_domain: "service.jnss.me" + +# host_vars/host/main.yml (explicit override) +service_domain: "service.jnss.me" +``` + +**Examples:** +- Authentik: `authentik_domain: "auth.jnss.me"` +- Nextcloud: `nextcloud_domain: "cloud.jnss.me"` + +### Advanced Service (Multiple Domains) + +For services that need separate domains for different purposes: + +```yaml +# roles/service/defaults/main.yml +service_http_domain: "service.jnss.me" # Web interface +service_api_domain: "api.jnss.me" # API endpoint +service_ssh_domain: "jnss.me" # SSH/CLI operations + +# host_vars/host/main.yml (explicit override) +service_http_domain: "service.jnss.me" +service_api_domain: "api.jnss.me" +service_ssh_domain: "jnss.me" +``` + +**Example:** +- Gitea: + - `gitea_http_domain: "git.jnss.me"` (web interface) + - `gitea_ssh_domain: "jnss.me"` (Git operations) + +--- + +## Usage in Templates + +### Caddy Configuration + +```jinja +# roles/service/templates/service.caddy.j2 +{{ service_domain }} { + reverse_proxy 127.0.0.1:{{ service_port }} +} +``` + +### Application Configuration + +```jinja +# roles/service/templates/service.conf.j2 +[server] +DOMAIN = {{ service_domain }} +ROOT_URL = https://{{ service_domain }}/ +``` + +### Task Display Messages + +```yaml +# roles/service/tasks/main.yml +- name: Display service information + debug: + msg: | + 🌐 Web Interface: https://{{ service_domain }} + 📍 Access your service at the domain above +``` + +--- + +## Domain Selection Guidelines + +### Use Root Domain When: +- Service is the primary purpose of the infrastructure +- You want cleaner URLs (e.g., SSH: `git@jnss.me` vs `git@git.jnss.me`) +- Industry standard uses root domain (e.g., GitHub uses `github.com` for SSH) + +### Use Subdomain When: +- Service is one of many +- You want explicit service identification +- You need clear separation between services + +### Use Different Domain When: +- Service needs to be on a different apex domain +- External service integration requires specific domain +- Multi-domain setup for geographical distribution + +--- + +## Examples by Service Type + +### Identity/Auth Service +```yaml +authentik_domain: "auth.jnss.me" +``` +**Rationale**: Auth subdomain is an industry standard + +### Storage Service +```yaml +nextcloud_domain: "cloud.jnss.me" +``` +**Rationale**: "cloud" clearly indicates storage/sync service + +### Git Service +```yaml +gitea_http_domain: "git.jnss.me" # Web UI +gitea_ssh_domain: "jnss.me" # SSH operations +``` +**Rationale**: +- HTTP uses `git.` for clarity +- SSH uses root domain to avoid `git@git.jnss.me` redundancy +- Matches GitHub/GitLab pattern + +### Monitoring Service +```yaml +grafana_domain: "monitor.jnss.me" +prometheus_domain: "metrics.jnss.me" +``` +**Rationale**: Different subdomains for different monitoring tools + +--- + +## Configuration Layers + +### 1. Role Defaults (`roles/service/defaults/main.yml`) + +Provide sensible defaults: + +```yaml +# Option A: Use specific domain (explicit) +service_domain: "service.jnss.me" + +# Option B: Use caddy_domain if it makes sense (flexible) +service_domain: "service.{{ caddy_domain | default('localhost') }}" + +# Recommendation: Use Option A for clarity +``` + +### 2. Host Variables (`host_vars/hostname/main.yml`) + +**Always explicitly set** in production: + +```yaml +# ================================================================= +# Service Configuration +# ================================================================= +service_domain: "service.jnss.me" +``` + +**Why explicit?** +- Clear what domain is configured +- Easy to change without understanding defaults +- Easier to audit configuration +- Documentation in configuration itself + +### 3. Group Variables (`group_vars/production/main.yml`) + +For settings shared across production hosts: + +```yaml +# Common production settings +service_enable_ssl: true +service_require_auth: true + +# Generally avoid setting domains in group_vars +# (domains are usually host-specific) +``` + +--- + +## Anti-Patterns to Avoid + +### ❌ Subdomain Composition + +```yaml +# DON'T DO THIS +service_subdomain: "service" +service_domain: "{{ caddy_domain }}" +service_full_domain: "{{ service_subdomain }}.{{ service_domain }}" +``` + +**Problems:** +- Complex (3 variables for 1 domain) +- Inflexible (can't use root or different domains) +- Forces inheritance from infrastructure variable +- Inconsistent with other services + +### ❌ Implicit Inheritance + +```yaml +# DON'T DO THIS +service_domain: "{{ caddy_domain }}" +``` + +**Problems:** +- Not explicit what domain is used +- Harder to change +- Hides actual configuration +- Requires understanding of infrastructure variables + +### ❌ Mixed Patterns + +```yaml +# DON'T DO THIS +authentik_domain: "auth.jnss.me" # Direct +nextcloud_subdomain: "cloud" # Composition +service_domain: "{{ caddy_domain }}" # Inheritance +``` + +**Problems:** +- Inconsistent +- Confusing for maintainers +- Different patterns for same purpose + +--- + +## Migration from Old Pattern + +If you have services using the old subdomain composition pattern: + +### Step 1: Identify Current Variables + +```yaml +# Old pattern +service_subdomain: "service" +service_domain: "{{ caddy_domain }}" +service_full_domain: "{{ service_subdomain }}.{{ service_domain }}" +``` + +### Step 2: Replace with Direct Domain + +```yaml +# New pattern +service_domain: "service.jnss.me" +``` + +### Step 3: Update Template References + +```jinja +# Old +{{ service_full_domain }} + +# New +{{ service_domain }} +``` + +### Step 4: Remove Unused Variables + +Delete `service_subdomain` and `service_full_domain` from defaults. + +### Step 5: Add Explicit Host Configuration + +```yaml +# host_vars/arch-vps/main.yml +service_domain: "service.jnss.me" +``` + +--- + +## Testing Domain Configuration + +### Verify Caddy Configuration + +```bash +# Check generated Caddy config +cat /etc/caddy/sites-enabled/service.caddy + +# Test Caddy configuration syntax +caddy validate --config /etc/caddy/Caddyfile + +# Check TLS certificate +curl -I https://service.jnss.me +``` + +### Verify Application Configuration + +```bash +# Check service configuration +cat /etc/service/config.ini | grep -i domain + +# Test service accessibility +curl https://service.jnss.me +``` + +### Verify DNS Resolution + +```bash +# Check DNS resolution +dig service.jnss.me + +# Test connectivity +nc -zv service.jnss.me 443 +``` + +--- + +## Checklist for New Services + +When creating a new service role: + +- [ ] Use direct domain specification (not subdomain composition) +- [ ] Define domain(s) in `roles/service/defaults/main.yml` +- [ ] Add explicit domain(s) to host_vars +- [ ] Update all templates to use domain variable(s) +- [ ] Document domain configuration in role README +- [ ] Follow naming convention: `service_domain` or `service_[type]_domain` +- [ ] Test with different domain configurations + +--- + +## Summary + +**Standard Pattern:** +```yaml +# Defaults: Provide reasonable default +service_domain: "service.jnss.me" + +# Host vars: Always explicit in production +service_domain: "service.jnss.me" + +# Templates: Use variable directly +{{ service_domain }} +``` + +**Key Principles:** +1. Direct and explicit +2. One variable per domain +3. No forced inheritance +4. Consistent across all services +5. Flexible for any domain pattern + +--- + +**Rick-Infra Domain Configuration Standard** +Simple, flexible, and consistent domain configuration for all services. diff --git a/host_vars/arch-vps/main.yml b/host_vars/arch-vps/main.yml index 1732bf7..49a0c51 100644 --- a/host_vars/arch-vps/main.yml +++ b/host_vars/arch-vps/main.yml @@ -72,6 +72,12 @@ nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}" nextcloud_service_enabled: true nextcloud_service_state: "started" +# ================================================================= +# Gitea Configuration +# ================================================================= +gitea_http_domain: "git.jnss.me" +gitea_ssh_domain: "jnss.me" + # ================================================================= # Security & Logging # ================================================================= diff --git a/now-what.md b/now-what.md index bc6aafc..353e741 100644 --- a/now-what.md +++ b/now-what.md @@ -12,8 +12,8 @@ - [ ] Contacts and calendars - [ ] Storage bucket integration? -- [ ] Gitea - - [ ] SSH setup +- [x] Gitea + - [x] SSH passthrough setup - [ ] Authentik Invitations for users? diff --git a/rick-infra.yml b/rick-infra.yml index 1bba12a..6e7f399 100644 --- a/rick-infra.yml +++ b/rick-infra.yml @@ -13,7 +13,7 @@ # Usage: # ansible-playbook playbooks/homelab.yml -- import_playbook: playbooks/security.yml +# - import_playbook: playbooks/security.yml - name: Deploy Homelab Infrastructure hosts: homelab become: true diff --git a/roles/gitea/README.md b/roles/gitea/README.md index ad44fe7..0540d31 100644 --- a/roles/gitea/README.md +++ b/roles/gitea/README.md @@ -8,8 +8,10 @@ Self-contained Gitea Git service for rick-infra following the established archit - ✅ **Native Arch installation**: Uses pacman packages - ✅ **PostgreSQL integration**: Uses shared PostgreSQL infrastructure - ✅ **Caddy integration**: Deploys reverse proxy configuration +- ✅ **Dual SSH modes**: Passthrough (default) or dedicated SSH server +- ✅ **Flexible domains**: Separate HTTP and SSH domains - ✅ **Security hardened**: SystemD restrictions and secure defaults -- ✅ **Firewall management**: Automatically configures nftables for SSH access +- ✅ **Firewall management**: Automatic nftables configuration per mode - ✅ **fail2ban protection**: Brute force protection for SSH authentication - ✅ **Production ready**: HTTPS, SSH access, LFS support @@ -17,11 +19,11 @@ Self-contained Gitea Git service for rick-infra following the established archit - **Dependencies**: PostgreSQL infrastructure role - **Database**: Self-managed gitea database and user -- **Network**: HTTP on :3000 (localhost), SSH on :2222 (public) -- **Web access**: https://git.domain.com (via Caddy reverse proxy) -- **SSH access**: ssh://git@git.domain.com:2222 -- **Firewall**: Port 2222 automatically opened via nftables -- **Security**: fail2ban monitors and blocks SSH brute force attempts +- **Network**: HTTP on :3000 (localhost), SSH via system SSH (port 22) or dedicated (port 2222) +- **Web access**: https://git.jnss.me (via Caddy reverse proxy) +- **SSH access**: git@jnss.me:user/repo.git (passthrough mode, default) +- **Firewall**: Managed per SSH mode (no extra ports in passthrough) +- **Security**: fail2ban protects SSH authentication (system or dedicated jail) ## Configuration @@ -31,11 +33,13 @@ Key variables (defaults in `defaults/main.yml`): # Service gitea_service_enabled: true gitea_http_port: 3000 -gitea_ssh_port: 2222 -# Domain -gitea_subdomain: "git" -gitea_domain: "{{ caddy_domain }}" +# Domain Configuration +gitea_http_domain: "git.jnss.me" # Web interface +gitea_ssh_domain: "jnss.me" # SSH/Git operations + +# SSH Mode +gitea_ssh_mode: "passthrough" # or "dedicated" # Database (self-managed) gitea_db_name: "gitea" @@ -46,36 +50,108 @@ gitea_db_password: "{{ vault_gitea_db_password }}" gitea_app_name: "Gitea: Git with a cup of tea" gitea_disable_registration: false gitea_enable_lfs: true - -# Firewall and Security -gitea_manage_firewall: true # Automatically manage nftables rules ``` +## Domain Configuration + +Gitea uses **separate domains** for HTTP and SSH access, providing flexibility and cleaner URLs: + +### HTTP Domain (`gitea_http_domain`) +- **Purpose**: Web interface access +- **Default**: `git.jnss.me` +- **Example**: `https://git.jnss.me` +- **Used for**: Browsing repos, managing settings, viewing commits + +### SSH Domain (`gitea_ssh_domain`) +- **Purpose**: Git clone/push/pull operations +- **Default**: `jnss.me` (root domain) +- **Example**: `git@jnss.me:user/repo.git` +- **Used for**: Git operations over SSH + +**Why Separate Domains?** +- ✅ Cleaner SSH URLs (no redundant "git" subdomain) +- ✅ Flexibility to use completely different domains +- ✅ Matches industry standards (GitHub, GitLab use root domain for SSH) +- ✅ Professional appearance + +**Configuration Examples:** +```yaml +# Standard setup (recommended) +gitea_http_domain: "git.jnss.me" +gitea_ssh_domain: "jnss.me" +# Result: git@jnss.me:user/repo.git + +# Same domain for both +gitea_http_domain: "git.jnss.me" +gitea_ssh_domain: "git.jnss.me" +# Result: git@git.jnss.me:user/repo.git + +# Completely custom +gitea_http_domain: "code.jnss.me" +gitea_ssh_domain: "git.example.com" +# Result: git@git.example.com:user/repo.git +``` + +## SSH Modes + +### Passthrough Mode (Default - Recommended) + +System SSH handles Git operations via AuthorizedKeysCommand: + +```bash +# Clone repository (standard Git URL, no port number) +git clone git@jnss.me:username/repository.git + +# Add as remote +git remote add origin git@jnss.me:username/repository.git + +# Test SSH connection +ssh -T git@jnss.me +``` + +**Features:** +- ✅ Standard Git URLs (no :2222 port) +- ✅ Single SSH daemon (smaller attack surface) +- ✅ System fail2ban protects everything +- ✅ Port 22 only (no extra firewall rules) +- ✅ Matches GitHub/GitLab pattern + +### Dedicated Mode (Fallback) + +Gitea runs its own SSH server on port 2222: + +```bash +# Clone repository (with port number) +git clone ssh://git@jnss.me:2222/username/repository.git + +# Add as remote +git remote add origin ssh://git@jnss.me:2222/username/repository.git + +# Test SSH connection +ssh -T -p 2222 git@jnss.me +``` + +**Features:** +- ✅ Complete isolation from system SSH +- ✅ Independent configuration +- ⚠️ Requires port 2222 open in firewall +- ⚠️ Non-standard URLs (requires :2222) + +**To switch modes:** +```yaml +# host_vars/arch-vps/main.yml +gitea_ssh_mode: "dedicated" # or "passthrough" +``` +Then re-run the playbook. + ## Usage 1. **Add vault password**: Set `vault_gitea_db_password` in host_vars vault -2. **Deploy**: `ansible-playbook site.yml --tags gitea` -3. **Access**: Visit https://git.yourdomain.com to set up admin account +2. **Configure domains** (optional): Override `gitea_http_domain` and `gitea_ssh_domain` in host_vars +3. **Deploy**: `ansible-playbook site.yml --tags gitea` +4. **Access**: Visit https://git.jnss.me to set up admin account -## Dependencies - -- PostgreSQL infrastructure role (auto-included) -- Caddy web server (for HTTPS access) -- Vault password: `vault_gitea_db_password` - -## SSH Access - -Gitea provides Git repository access via SSH on port 2222: - -```bash -# Clone a repository -git clone ssh://git@git.jnss.me:2222/username/repository.git - -# Or add as remote -git remote add origin ssh://git@git.jnss.me:2222/username/repository.git -``` - -### SSH Key Setup +## SSH Key Setup 1. **Generate SSH key** (if you don't have one): ```bash @@ -95,40 +171,52 @@ git remote add origin ssh://git@git.jnss.me:2222/username/repository.git 4. **Test SSH connection**: ```bash - ssh -T -p 2222 git@git.jnss.me + # Passthrough mode + ssh -T git@jnss.me + + # Dedicated mode + ssh -T -p 2222 git@jnss.me ``` ## Firewall and Security ### Automatic Firewall Management -The Gitea role automatically manages firewall rules via nftables: +Firewall configuration is mode-aware: -- **Port 2222** is opened automatically when Gitea is deployed -- Firewall rules are stored in `/etc/nftables.d/gitea.nft` -- Rules are integrated with the main security playbook configuration -- To disable automatic firewall management, set `gitea_manage_firewall: false` +**Passthrough Mode:** +- No extra firewall rules needed (uses port 22) +- System SSH already configured in security playbook + +**Dedicated Mode:** +- Port 2222 automatically opened via nftables +- Firewall rules stored in `/etc/nftables.d/50-gitea.nft` +- Rules integrated with main security playbook +- Automatically removed when switching to passthrough ### fail2ban Protection -SSH brute force protection is automatically configured: +Protection is mode-aware: -- **Jail**: `gitea-ssh` monitors Gitea SSH authentication attempts -- **Max retries**: 5 failed attempts -- **Find time**: 10 minutes (600 seconds) -- **Ban time**: 1 hour (3600 seconds) -- **Action**: IP banned via nftables +**Passthrough Mode:** +- System `sshd` jail protects all SSH traffic (port 22) +- Covers admin SSH + Git operations automatically +- No separate Gitea jail needed + +**Dedicated Mode:** +- `gitea-ssh` jail monitors Gitea logs (port 2222) +- Max retries: 5 failed attempts +- Find time: 10 minutes (600 seconds) +- Ban time: 1 hour (3600 seconds) +- Action: IP banned via nftables Check fail2ban status: ```bash -# Check Gitea SSH jail status +# Passthrough mode +fail2ban-client status sshd + +# Dedicated mode fail2ban-client status gitea-ssh - -# View banned IPs -fail2ban-client get gitea-ssh banned - -# Unban an IP if needed -fail2ban-client set gitea-ssh unbanip 203.0.113.100 ``` ### Firewall Verification @@ -137,13 +225,20 @@ fail2ban-client set gitea-ssh unbanip 203.0.113.100 # List active nftables rules nft list ruleset -# Check if Gitea SSH port is open +# Check for Gitea SSH port (should be empty in passthrough) nft list ruleset | grep 2222 -# Verify from external machine -nc -zv git.jnss.me 2222 +# Verify SSH connectivity +nc -zv jnss.me 22 # Passthrough +nc -zv jnss.me 2222 # Dedicated ``` +## Dependencies + +- PostgreSQL infrastructure role (auto-included) +- Caddy web server (for HTTPS access) +- Vault password: `vault_gitea_db_password` + ## Self-Contained Design This role follows rick-infra's self-contained service pattern: @@ -151,9 +246,18 @@ This role follows rick-infra's self-contained service pattern: - Manages its own configuration and data - Deploys its own Caddy reverse proxy config - Manages its own firewall rules and security (nftables, fail2ban) +- Flexible domain configuration (not tied to infrastructure variables) - Independent lifecycle from other services +## Migration Guide + +See `docs/gitea-ssh-migration-guide.md` for: +- Switching between SSH modes +- Updating Git remote URLs +- Bulk migration scripts +- Troubleshooting + --- **Rick-Infra Gitea Service** -Git repository management with integrated CI/CD capabilities. \ No newline at end of file +Git repository management with flexible SSH modes and domain configuration. diff --git a/roles/gitea/defaults/main.yml b/roles/gitea/defaults/main.yml index a78c630..25c6321 100644 --- a/roles/gitea/defaults/main.yml +++ b/roles/gitea/defaults/main.yml @@ -20,16 +20,14 @@ gitea_home: "/var/lib/gitea" # Network Configuration gitea_http_port: 3000 -gitea_ssh_port: 2222 # ================================================================= # Domain and Caddy Integration # ================================================================= # Domain setup (follows rick-infra pattern) -gitea_subdomain: "git" -gitea_domain: "{{ caddy_domain | default('localhost') }}" -gitea_full_domain: "{{ gitea_subdomain }}.{{ gitea_domain }}" +gitea_http_domain: "git.jnss.me" +gitea_ssh_domain: "jnss.me" # Caddy integration caddy_sites_enabled_dir: "/etc/caddy/sites-enabled" @@ -63,15 +61,34 @@ gitea_enable_lfs: true gitea_disable_registration: false gitea_require_signin: false -# SSH settings -gitea_start_ssh_server: true +# ================================================================= +# SSH Mode Configuration +# ================================================================= + +# SSH Mode: 'passthrough' or 'dedicated' +# - passthrough (default): Use system SSH on port 22 +# * More secure (single SSH daemon, smaller attack surface) +# * Standard Git URLs (no :2222 port number needed) +# * System fail2ban automatically protects Git operations +# * Recommended for production use +# +# - dedicated (fallback): Run Gitea's built-in SSH server on port 2222 +# * Complete isolation from system SSH +# * Independent configuration and restarts +# * Requires opening port 2222 in firewall +# * Useful for debugging or when passthrough causes issues +gitea_ssh_mode: "passthrough" + +# Dynamic SSH configuration based on mode +gitea_ssh_port: "{{ 22 if gitea_ssh_mode == 'passthrough' else 2222 }}" +gitea_start_ssh_server: "{{ false if gitea_ssh_mode == 'passthrough' else true }}" # ================================================================= # Firewall Configuration # ================================================================= -# Firewall management -gitea_manage_firewall: true # Set to false if firewall is managed externally +# Firewall management (only opens port in dedicated mode) +gitea_manage_firewall: "{{ true if gitea_ssh_mode == 'dedicated' else false }}" # ================================================================= # Infrastructure Dependencies (Read-only) @@ -91,4 +108,4 @@ postgresql_port: 5432 # - Creates its own database and user # - Deploys Caddy configuration to sites-enabled # - Uses native Arch Linux Gitea package -# - Follows self-contained service pattern \ No newline at end of file +# - Follows self-contained service pattern diff --git a/roles/gitea/handlers/main.yml b/roles/gitea/handlers/main.yml index accbf03..464bc58 100644 --- a/roles/gitea/handlers/main.yml +++ b/roles/gitea/handlers/main.yml @@ -27,4 +27,11 @@ - name: restart fail2ban systemd: name: fail2ban - state: restarted \ No newline at end of file + state: restarted + +- name: restart sshd + systemd: + name: sshd + state: restarted + # Safety: only restart if not running locally + when: ansible_connection != 'local' \ No newline at end of file diff --git a/roles/gitea/tasks/fail2ban.yml b/roles/gitea/tasks/fail2ban.yml index ac5ef09..96497c7 100644 --- a/roles/gitea/tasks/fail2ban.yml +++ b/roles/gitea/tasks/fail2ban.yml @@ -1,7 +1,7 @@ --- # Gitea fail2ban Configuration - Rick-Infra -# Protects Gitea SSH from brute force attacks -# Integrates with system fail2ban service +# Mode-aware: Only protects dedicated mode (port 2222) +# In passthrough mode, system 'sshd' jail protects port 22 - name: Install fail2ban pacman: @@ -13,6 +13,7 @@ content: | # Fail2ban filter for Gitea SSH authentication failures # Rick-Infra: Gitea role + # Only used in dedicated mode (port {{ gitea_ssh_port }}) [Definition] # Match failed authentication attempts in Gitea logs @@ -33,14 +34,17 @@ modification_time: preserve access_time: preserve -- name: Add Gitea SSH jail to fail2ban +- name: Add Gitea SSH jail to fail2ban (mode-aware) blockinfile: path: /etc/fail2ban/jail.local marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH" block: | # Gitea SSH Protection - Rick-Infra + # Mode: {{ gitea_ssh_mode }} + # - dedicated: Monitors Gitea logs on port {{ gitea_ssh_port }} + # - passthrough: Disabled (system 'sshd' jail protects port 22) [gitea-ssh] - enabled = true + enabled = {{ 'true' if gitea_ssh_mode == 'dedicated' else 'false' }} port = {{ gitea_ssh_port }} filter = gitea-ssh logpath = {{ gitea_home }}/log/gitea.log @@ -57,19 +61,61 @@ enabled: yes state: started -- name: Add fail2ban restart handler +- name: Flush handlers to ensure fail2ban restarts meta: flush_handlers -- name: Display fail2ban status for Gitea +- name: Wait for fail2ban to be ready + pause: + seconds: 2 + +- name: Verify gitea-ssh jail status (dedicated mode only) + command: fail2ban-client status gitea-ssh + register: gitea_jail_verify + changed_when: false + failed_when: false + when: gitea_ssh_mode == 'dedicated' + +- name: Verify sshd jail status (passthrough mode) + command: fail2ban-client status sshd + register: sshd_jail_verify + changed_when: false + failed_when: false + when: gitea_ssh_mode == 'passthrough' + +- name: Display fail2ban configuration status debug: msg: | - 🛡️ fail2ban configured for Gitea SSH - 📍 Filter: /etc/fail2ban/filter.d/gitea-ssh.conf - 📍 Jail: gitea-ssh (in /etc/fail2ban/jail.local) - 🔒 Protection: Port {{ gitea_ssh_port }} - ⏱️ Ban time: 1 hour (3600 seconds) - 🔢 Max retries: 5 attempts in 10 minutes + 🛡️ fail2ban Protection for Gitea SSH + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 📍 Mode: {{ gitea_ssh_mode | upper }} - Check status: fail2ban-client status gitea-ssh + {% if gitea_ssh_mode == 'dedicated' %} + 📍 Jail: gitea-ssh + 📍 Port: {{ gitea_ssh_port }} + 📍 Status: {{ 'Active ✅' if gitea_jail_verify.rc == 0 else 'Not Active ⚠️' }} + 📍 Filter: /etc/fail2ban/filter.d/gitea-ssh.conf + 📍 Logfile: {{ gitea_home }}/log/gitea.log + + Protection Settings: + • Max retries: 5 attempts + • Find time: 10 minutes (600 seconds) + • Ban time: 1 hour (3600 seconds) + + Check status: + fail2ban-client status gitea-ssh + + {% else %} + 📍 Jail: sshd (system jail) + 📍 Port: 22 + 📍 Status: {{ 'Active ✅' if sshd_jail_verify.rc == 0 else 'Not Active ⚠️' }} + 📍 Coverage: All SSH traffic including Gitea Git operations + + Note: In passthrough mode, the system 'sshd' jail automatically + protects all SSH traffic on port 22, including Gitea Git + operations. No separate gitea-ssh jail is needed. + + Check status: + fail2ban-client status sshd + {% endif %} # Rick-Infra: Self-contained fail2ban protection per role diff --git a/roles/gitea/tasks/main.yml b/roles/gitea/tasks/main.yml index fc261bd..437b8b9 100644 --- a/roles/gitea/tasks/main.yml +++ b/roles/gitea/tasks/main.yml @@ -16,23 +16,30 @@ name: gitea state: present -# Firewall configuration - self-managed by Gitea role -- name: Configure firewall for Gitea SSH - import_tasks: firewall.yml - tags: ['firewall'] - when: gitea_manage_firewall | default(true) +# SSH Mode Configuration - Conditional based on gitea_ssh_mode +# Mode determines how Git SSH operations are handled -# fail2ban protection - self-managed by Gitea role -- name: Configure fail2ban for Gitea SSH - import_tasks: fail2ban.yml - tags: ['fail2ban', 'security'] - when: gitea_manage_firewall | default(true) +- name: Configure SSH passthrough mode (default) + import_tasks: ssh_passthrough.yml + when: gitea_ssh_mode == "passthrough" + tags: ['ssh', 'passthrough'] + +- name: Configure SSH dedicated mode (fallback) + import_tasks: ssh_dedicated.yml + when: gitea_ssh_mode == "dedicated" + tags: ['ssh', 'dedicated'] - name: Install Git pacman: name: git state: present +- name: Create Gitea group + group: + name: "{{ gitea_group }}" + system: yes + state: present + - name: Create Gitea user and group user: name: "{{ gitea_user }}" @@ -156,8 +163,8 @@ msg: | ✅ Gitea Git service deployed successfully! - 🌐 Web Interface: https://{{ gitea_full_domain }} - 🔗 SSH Clone: ssh://git@{{ gitea_full_domain }}:{{ gitea_ssh_port }} + 🌐 Web Interface: https://{{ gitea_http_domain }} + 🔗 SSH Clone: ssh://git@{{ gitea_ssh_domain }}:{{ gitea_ssh_port }} 📦 Local HTTP: http://127.0.0.1:{{ gitea_http_port }} 🗄️ Database: {{ gitea_db_name }} (self-managed) diff --git a/roles/gitea/tasks/ssh_dedicated.yml b/roles/gitea/tasks/ssh_dedicated.yml new file mode 100644 index 0000000..c444a7c --- /dev/null +++ b/roles/gitea/tasks/ssh_dedicated.yml @@ -0,0 +1,74 @@ +--- +# Gitea Dedicated SSH Server Configuration - Rick-Infra +# Configures Gitea to run its own SSH server on port 2222 +# This is the fallback mode when passthrough is not desired + +- name: Configure firewall for Gitea SSH (dedicated mode) + import_tasks: firewall.yml + tags: ['firewall'] + +- name: Configure fail2ban for Gitea SSH (dedicated mode) + import_tasks: fail2ban.yml + tags: ['fail2ban', 'security'] + +- name: Wait for fail2ban to be ready + pause: + seconds: 2 + +- name: Verify gitea-ssh jail is active + command: fail2ban-client status gitea-ssh + register: gitea_jail_status + changed_when: false + failed_when: false + +- name: Display fail2ban protection status + debug: + msg: | + 🛡️ Gitea SSH fail2ban protection: + {% if gitea_jail_status.rc == 0 %} + ✅ gitea-ssh jail is ACTIVE + {{ gitea_jail_status.stdout }} + {% else %} + ⚠️ WARNING: gitea-ssh jail not active! + This is a security risk - port {{ gitea_ssh_port }} is vulnerable to brute force attacks. + {% endif %} + +- name: Fail if gitea-ssh jail is not running (security critical) + fail: + msg: | + SECURITY ERROR: gitea-ssh fail2ban jail is not active! + Port {{ gitea_ssh_port }} is exposed but not protected. + Check fail2ban configuration and logs. + when: gitea_jail_status.rc != 0 + +- name: Remove SSH passthrough configuration if present + blockinfile: + path: /etc/ssh/sshd_config + marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH Passthrough" + state: absent + backup: yes + register: sshd_config_cleaned + notify: restart sshd + +- name: Remove AuthorizedKeysCommand script if present + file: + path: /usr/local/bin/gitea-keys + state: absent + +- name: Display dedicated mode configuration + debug: + msg: | + 🔧 Gitea SSH Mode: DEDICATED + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 📍 SSH Server: Gitea built-in (port {{ gitea_ssh_port }}) + 🔗 Clone URL: ssh://git@{{ gitea_ssh_domain }}:{{ gitea_ssh_port }}/user/repo.git + 🔥 Firewall: Port {{ gitea_ssh_port }} opened (nftables) + 🛡️ fail2ban: gitea-ssh jail protecting port {{ gitea_ssh_port }} + + Test connection: + ssh -T -p {{ gitea_ssh_port }} git@{{ gitea_ssh_domain }} + + Clone repository: + git clone ssh://git@{{ gitea_ssh_domain }}:{{ gitea_ssh_port }}/username/repo.git + +# Rick-Infra: Self-contained dedicated SSH mode with full security diff --git a/roles/gitea/tasks/ssh_passthrough.yml b/roles/gitea/tasks/ssh_passthrough.yml new file mode 100644 index 0000000..7ea5ba8 --- /dev/null +++ b/roles/gitea/tasks/ssh_passthrough.yml @@ -0,0 +1,128 @@ +--- +# Gitea SSH Passthrough Configuration - Rick-Infra +# Configures system SSH to handle Gitea Git authentication +# This is the default mode: more secure, standard Git URLs + +- name: Ensure OpenSSH server is installed + pacman: + name: openssh + state: present + +- name: Create Gitea AuthorizedKeysCommand script + template: + src: gitea-keys.sh.j2 + dest: /usr/local/bin/gitea-keys + mode: '0755' + owner: root + group: root + register: gitea_keys_script + +- name: Configure SSH for Gitea passthrough + blockinfile: + path: /etc/ssh/sshd_config + marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH Passthrough" + block: | + # Gitea SSH Passthrough - Rick-Infra + # System SSH delegates git user authentication to Gitea + # This allows standard Git URLs: git@{{ gitea_ssh_domain }}:user/repo.git + Match User {{ gitea_user }} + AuthorizedKeysCommandUser {{ gitea_user }} + AuthorizedKeysCommand /usr/local/bin/gitea-keys %u %t %k + AllowTcpForwarding no + AllowAgentForwarding no + X11Forwarding no + PermitTTY no + backup: yes + validate: '/usr/bin/sshd -t -f %s' + register: sshd_config_changed + notify: restart sshd + +- name: Verify SSH configuration syntax + command: sshd -t + changed_when: false + register: sshd_test + +- name: Display SSH validation result + debug: + msg: | + {% if sshd_test.rc == 0 %} + ✅ SSH configuration is valid + {% else %} + ⚠️ SSH configuration test failed: + {{ sshd_test.stderr }} + {% endif %} + +- name: Fail if SSH configuration is invalid + fail: + msg: "SSH configuration test failed. Rolling back changes." + when: sshd_test.rc != 0 + +- name: Remove Gitea firewall rule (passthrough uses port 22) + file: + path: /etc/nftables.d/50-gitea.nft + state: absent + notify: reload nftables + register: firewall_cleaned + +- name: Reload nftables if firewall rule was removed + systemd: + name: nftables + state: reloaded + when: firewall_cleaned.changed + +- name: Configure fail2ban for passthrough mode + import_tasks: fail2ban.yml + tags: ['fail2ban', 'security'] + +- name: Flush handlers to ensure sshd restarts if needed + meta: flush_handlers + +- name: Wait for SSH service to be available after restart + wait_for: + port: 22 + host: "{{ ansible_host | default('127.0.0.1') }}" + timeout: 30 + delegate_to: localhost + become: false + when: sshd_config_changed.changed + +- name: Test SSH connection after configuration + ping: + when: sshd_config_changed.changed + +- name: Verify passthrough is working + command: sudo -u {{ gitea_user }} /usr/local/bin/gitea-keys ssh-rsa test + register: gitea_keys_test + changed_when: false + failed_when: false + +- name: Display passthrough mode configuration + debug: + msg: | + 🔧 Gitea SSH Mode: PASSTHROUGH (Default) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 📍 SSH Server: System SSH (port 22) + 🔗 Clone URL: git@{{ gitea_ssh_domain }}:user/repo.git + 🔥 Firewall: Port 2222 closed (not needed) + 🛡️ fail2ban: System 'sshd' jail protects all SSH traffic + 🔑 AuthorizedKeysCommand: /usr/local/bin/gitea-keys + + How it works: + 1. User connects: ssh git@{{ gitea_ssh_domain }} + 2. System SSH checks: /usr/local/bin/gitea-keys + 3. Script queries Gitea database for SSH key + 4. If authorized, Gitea handles Git operation + + Test connection: + ssh -T git@{{ gitea_ssh_domain }} + + Clone repository: + git clone git@{{ gitea_ssh_domain }}:username/repo.git + + Benefits: + ✅ Standard Git URLs (no :2222 port number) + ✅ Single SSH daemon (smaller attack surface) + ✅ System fail2ban protects everything + ✅ One port to manage and monitor + +# Rick-Infra: Self-contained SSH passthrough mode with enhanced security diff --git a/roles/gitea/templates/app.ini.j2 b/roles/gitea/templates/app.ini.j2 index 091ef5a..cd3ddcd 100644 --- a/roles/gitea/templates/app.ini.j2 +++ b/roles/gitea/templates/app.ini.j2 @@ -11,14 +11,17 @@ DEFAULT_BRANCH = {{ gitea_default_branch }} [server] PROTOCOL = http -DOMAIN = {{ gitea_full_domain }} +DOMAIN = {{ gitea_http_domain }} HTTP_PORT = {{ gitea_http_port }} -ROOT_URL = https://{{ gitea_full_domain }}/ +ROOT_URL = https://{{ gitea_http_domain }}/ DISABLE_SSH = false +# SSH Mode: {{ gitea_ssh_mode }} START_SSH_SERVER = {{ gitea_start_ssh_server | lower }} -SSH_DOMAIN = {{ gitea_full_domain }} +SSH_DOMAIN = {{ gitea_ssh_domain }} SSH_PORT = {{ gitea_ssh_port }} +{% if gitea_ssh_mode == 'dedicated' %} SSH_LISTEN_PORT = {{ gitea_ssh_port }} +{% endif %} LOCAL_ROOT_URL = http://127.0.0.1:{{ gitea_http_port }}/ APP_DATA_PATH = {{ gitea_home }}/data @@ -47,7 +50,7 @@ DISABLE_REGISTRATION = {{ gitea_disable_registration | lower }} REQUIRE_SIGNIN_VIEW = {{ gitea_require_signin | lower }} DEFAULT_KEEP_EMAIL_PRIVATE = true DEFAULT_ALLOW_CREATE_ORGANIZATION = true -NO_REPLY_ADDRESS = noreply.{{ gitea_domain }} +NO_REPLY_ADDRESS = noreply@{{ gitea_http_domain }} [log] MODE = console @@ -63,4 +66,4 @@ CONTENT_PATH = {{ gitea_home }}/data/lfs [git] PATH = /usr/bin/git -# Rick-Infra: Simplified Gitea configuration for self-contained service \ No newline at end of file +# Rick-Infra: Simplified Gitea configuration for self-contained service diff --git a/roles/gitea/templates/gitea-keys.sh.j2 b/roles/gitea/templates/gitea-keys.sh.j2 new file mode 100644 index 0000000..255573c --- /dev/null +++ b/roles/gitea/templates/gitea-keys.sh.j2 @@ -0,0 +1,28 @@ +#!/bin/bash +# Gitea SSH Keys AuthorizedKeysCommand - Rick-Infra +# Generated by Ansible Gitea role +# +# This script is called by OpenSSH's AuthorizedKeysCommand to query +# Gitea's database for SSH public keys when the 'git' user connects. +# +# Called by SSH with parameters: +# %u = username (should be "git") +# %t = key type (ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, etc.) +# %k = base64 encoded public key content +# +# The script returns authorized_keys format entries that include +# forced commands to execute Gitea's Git server. + +set -euo pipefail + +# Gitea keys command queries the database and returns authorized_keys format +# If the key is found, it returns a line like: +# command="/usr/bin/gitea serv key-123",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA... + +exec /usr/bin/gitea keys \ + --config /etc/gitea/app.ini \ + --username "$1" \ + --type "$2" \ + --content "$3" + +# Rick-Infra: AuthorizedKeysCommand for Gitea SSH passthrough mode diff --git a/roles/gitea/templates/gitea.caddy.j2 b/roles/gitea/templates/gitea.caddy.j2 index 7379025..bee4ee2 100644 --- a/roles/gitea/templates/gitea.caddy.j2 +++ b/roles/gitea/templates/gitea.caddy.j2 @@ -2,7 +2,7 @@ # Generated by Ansible Gitea role # Deployed to {{ caddy_sites_enabled_dir }}/gitea.caddy -{{ gitea_full_domain }} { +{{ gitea_http_domain }} { # Reverse proxy to Gitea reverse_proxy 127.0.0.1:{{ gitea_http_port }} @@ -29,4 +29,4 @@ } } -# Rick-Infra: Self-contained Gitea service with Caddy reverse proxy \ No newline at end of file +# Rick-Infra: Self-contained Gitea service with Caddy reverse proxy