# Linux Dev Station - dev-02.lab.alexpires.me ## Overview Provisioned `dev-02.lab.alexpires.me` (Fedora, `10.19.4.210`) as a remote dev station mirroring the macOS setup. Includes code-server, opencode, and dev toolchain management. Exposed via nginx reverse proxy with TLS termination and OAuth2/OIDC SSO (Gitea as IdP). ## Role: `linux_dev_station` - **Location:** `ansible/roles/linux_dev_station/` - **Scope:** Fedora only (`ansible_facts['distribution'] == "Fedora"`) - **Package manager:** dnf for system packages; tool-managed (uv, rustup, fnm) for dev toolchains - **Excludes:** podman/container runtime management ### Components - **SSH hardening:** Public key auth, password auth disabled, root login disabled - **Users:** `devstation` user with sudo, zsh shell, and required groups - **Zsh:** Oh My Zsh with agnoster theme, plugins, custom env vars, and zshrc management - **Tooling:** - `uv` - Python package manager - `rustup` - Rust toolchain - `fnm` + `node` - Node.js version manager - `code-server` - VS Code remote on `127.0.0.1:3000` - `opencode` - AI coding assistant (HTTP server on `127.0.0.1:4096`) - **Firewall:** firewalld with `fw_allowed_ports` (22, 443) - **Systemd services:** code-server, opencode ### Opencode Details - **Repo:** `anomalyco/opencode` (not `opencode-ai`) - **Version:** `1.17.18` - **Binary:** `opencode-linux-x64.tar.gz` - **Mode:** HTTP server (`serve`), binds to `127.0.0.1` - **Install strategy:** Versioned directories (`~/.local/lib/tool-vX.Y.Z`) + symlinks for idempotent upgrades ### Oh My Zsh Plugins (Linux) git, fzf, common-aliases, sudo, tmux, extract, colored-man-pages, history-substring-search, kubectl, microk8s, docker, docker-compose, ansible, aws, golang, dnf, bgnotify ## macOS Role Updates - **Location:** `ansible/roles/macos_dev_station/` - Synced Oh My Zsh fixes: sourcing, theme, plugins, extra env vars - **Oh My Zsh Plugins (macOS):** git, fzf, common-aliases, sudo, tmux, extract, colored-man-pages, history-substring-search, kubectl, docker, docker-compose, ansible, aws, golang, brew, bgnotify ## TLS + OAuth2/OIDC SSO (`nginx_proxy` role) - **Location:** `ansible/roles/nginx_proxy/` — standalone, generic role not tied to `linux_dev_station` - **Playbook:** `ansible/playbook_nginx_proxy.yml` targets `[nginx]` group - **Architecture:** nginx (:443, TLS) → oauth2-proxy (:4180, session mgmt) → backends (127.0.0.1 only) - **Certificate:** SAN cert for `ide.lab.alexpires.me` + `agent.lab.alexpires.me` via certbot DNS-01 (cloudns plugin), auto-renewal cron - **SSO cookie:** Scoped to `.lab.alexpires.me` - **IdP:** Gitea (`code.alexpires.me`), OAuth2 app `lab-services` (confidential client, scopes: `openid`, `profile`, `email`) - **oauth2-proxy:** v7.15.3, config format: `oidc_issuer_url`, `http_address`, `upstreams` array, `cookie_domains` array, `proxy_websockets = true`, `reverse_proxy = true` - **Services exposed:** - `https://ide.lab.alexpires.me` → code-server (127.0.0.1:3000) - `https://agent.lab.alexpires.me` → opencode (127.0.0.1:4096) - **Nginx fixes applied:** `types_hash_bucket_size 64;`, ssl_ciphers single line, http2 separate directive, SAN cert shared via first site directory, POST auth_request body fix (`proxy_set_header Content-Length "";`), proxy_buffer_size for oauth2 callback ### Defaults - `nginx_proxy_oauth2_proxy_version: "7.15.3"` - `nginx_proxy_cloudns_nameserver: "109.201.133.111"` - `nginx_proxy_sites` — list of site configs with server_name, upstream, websocket toggle ### Secrets - `OAUTH2_PROXY_COOKIE_SECRET` — 32-byte random, stored in vault - `OAUTH2_PROXY_CLIENT_ID` / `OAUTH2_PROXY_CLIENT_SECRET` — from Gitea OAuth2 app - `CLOUDNS_AUTH_ID` / `CLOUDNS_PASSWORD` — DNS credentials for certbot ## Infrastructure Changes - **DNS:** - Added A record for `dev-02.lab.alexpires.me` (`10.19.4.210`) - Added A records for `ide` and `agent` → `10.19.4.210` (in `terraform/apps/dev-01/config.tf`) - **Inventory:** Added `[nginx]` group with `dev-02.lab.alexpires.me`; dev-02 removed from `[certbot]` group - **Host vars:** `ansible/host_vars/dev-02.lab.alexpires.me.yml` with `nginx_proxy_sites`, `bind_to_localhost: true`, firewall rules (22, 443), OAuth2 credentials via `lookup('env', ...)` - **dev-01 cleanup:** Removed `opencode`/`vscode` CNAME records and `proxy_services` entries (no longer proxying to dev-02) ## Service Bindings | Service | Binding | |---------|---------| | code-server | `127.0.0.1:3000` | | opencode | `127.0.0.1:4096` | | oauth2-proxy | `127.0.0.1:4180` | | nginx | `0.0.0.0:443` | ## Firewall | Port | Access | |------|--------| | 22 | `10.19.4.0/24` + VPN (`10.5.5.5/32`) | | 443 | `10.19.4.0/24` + VPN (`10.5.5.5/32`) | | 3000/4096 | Closed (nginx handles all external access) | ## SELinux - http_port_t extended to include 4180 (oauth2-proxy) - httpd_can_network_connect enabled - `/etc/letsencrypt` context set to `httpd_cert_t` ## Known Issues & Workarounds - **ssh-agent systemd:** Removed due to SELinux blocking transitions; managed via `.zshrc` startup instead - **ACL issue:** `allow_world_readable_tmpfiles = True` required in `ansible.cfg` for macOS→Fedora `become_user` compatibility ## Required Credentials - `GITEA_APP_TOKEN` - `OPEN_WEBUI_API_KEY` - `CLOUDNS_AUTH_ID` / `CLOUDNS_PASSWORD` (for certbot DNS-01) - `OAUTH2_PROXY_COOKIE_SECRET` / `CLIENT_ID` / `CLIENT_SECRET` (for nginx_proxy role) ## Ansible.cfg Fix Added `allow_world_readable_tmpfiles = True` under `[defaults]` to fix `become_user` failures on Fedora when running from macOS.