- Updated AGENTS.md to include new directories for execution plans and implemented feature reports. - Modified Ansible host variables for dev-02 and gpu-01 to change API endpoints and add new configurations for Scaleway. - Adjusted firewall rules in gpu-01 to allow HTTPS traffic instead of the previous Ollama port. - Added OAuth2 proxy configurations to gpu-01 for enhanced security. - Removed deprecated open-webui module from Terraform app configurations. - Updated Terraform configurations to reflect changes in local variables and removed unused secrets. - Created a new sectool.json file for CloudNS integration with Bitwarden. - Enhanced SELinux configurations for nginx to allow connections to any port and added oauth2-proxy port.
12 KiB
Plan: Replace OpenWebUI with llama.cpp behind nginx proxy
Current State
- llama.cpp runs on
gpu-01.lab.alexpires.mevia Podman on port11434(bound to127.0.0.1with API key) - nginx proxy role runs on
dev-02.lab.alexpires.meandgpu-01.lab.alexpires.me - OpenWebUI has been decommissioned (removed from K8s, DNS, inventory)
- DNS for
lab.alexpires.meis managed via Terraform (terraform/apps/dev-01/config.tfzones) - oauth2-proxy template uses OIDC provider (
code.alexpires.me),set_xauthrequest = true
Goals
- Deploy llama.cpp behind nginx proxy at
llm.lab.alexpires.me✅ - API paths (
/api/*) accessible without OAuth2 (token auth enforced via--api-key) ✅ - All other paths (chat UI, etc.) protected with OAuth2 via oauth2-proxy ✅
- Issue SSL certificate for
llm.lab.alexpires.mevia CloudNS DNS ✅ - OpenWebUI decommissioned ✅
Important Constraint
Resolved. Phase 2 completed: llama.cpp now bound to
127.0.0.1with API key. OpenWebUI decommissioned.
Changes Required
All changes have been implemented. See execution order below for completed steps.
1. Add DNS record for llm.lab.alexpires.me ✅
File: terraform/apps/dev-01/config.tf
Add A record to the lab.alexpires.me zone:
{
name = "llm"
type = "A"
content = "10.19.4.106"
}
Run tofu plan/apply on terraform/apps/dev-01/ before Phase 1 so DNS resolves.
2. Add gpu-01 to [nginx] inventory group ✅
3. Update nginx-server.conf.j2 — per-site no_auth conditional ✅
4. Configure gpu-01 host vars for nginx proxy ✅
5. Add firewall rules for HTTPS ✅
6. Update llama_server role — support configurable bind address ✅
7. Two-phase transition ✅
Phase 1 — Deploy nginx proxy (llama.cpp still on 0.0.0.0, no API key)
- Keep
llama_server_bind: "0.0.0.0"(current behavior, default) - Do NOT set API key
- nginx proxies to
127.0.0.1:11434(container is bound to0.0.0.0:11434, reachable from localhost) - OpenWebUI continues to work (still connects to
gpu-01:11434directly) - Test:
llm.lab.alexpires.me/requires OAuth2 login,llm.lab.alexpires.me/api/*is open
Phase 2 — Switch to localhost binding + enable API key (after validation)
- Change
llama_server_bind: "127.0.0.1"in host vars - Add
--api-keytollama_server_argv_extrain host vars:llama_server_argv_extra: - "--api-key" - "{{ lookup('env', 'LLAMA_SERVER_API_KEY') }}" # ... existing args ... - OpenWebUI will break (can no longer reach llama.cpp on
gpu-01:11434) — decommission OpenWebUI - Verify all consumers migrate to
llm.lab.alexpires.me/api/*withAuthorization: Bearer <key> - Remove old firewall rules for port 11434 (no longer externally reachable)
Execution Order
Phase 1 — Deploy nginx proxy (llama.cpp unchanged, OpenWebUI still works)
Status: COMPLETE — All steps executed and verified.
-
Update Terraform DNS — add
llmA record interraform/apps/dev-01/config.tf, runtofu apply✅ -
Update inventory — add gpu-01 to
[nginx]group, clean up duplicate[certbot]group ✅ -
Update nginx-server.conf.j2 — add
no_authper-site conditional (template stays generic) ✅ -
Update llama_server role — add
llama_server_bindvariable (default0.0.0.0) ✅ -
Update host vars — add nginx sites (with
extra_locationsfor/api/), OAuth2 creds, firewall rules ✅ -
Run nginx proxy playbook — installs nginx + oauth2-proxy + certbot + deploys config ✅
-
Fix SELinux — add
nginx_connect_anyboolean + port 4180 tohttp_port_t(manually applied, added to role for persistence) ✅ -
Verify chat UI —
llm.lab.alexpires.me/returns oauth2-proxy login page (200) ✅ -
Verify API —
llm.lab.alexpires.me/api/v1/modelsreturns 200 with models list ✅ -
Verify OpenWebUI still works — llama.cpp still bound to
0.0.0.0:11434✅ -
Update Terraform DNS — add
llmA record interraform/apps/dev-01/config.tf, runtofu apply -
Update inventory — add gpu-01 to
[nginx]group, clean up duplicate[certbot]group -
Update nginx-server.conf.j2 — add
no_authper-site conditional (template stays generic) -
Update llama_server role — add
llama_server_bindvariable (default0.0.0.0) -
Update host vars — add nginx sites (with
extra_locationsfor/api/), OAuth2 creds, firewall rules -
Run nginx proxy playbook — installs nginx + oauth2-proxy + certbot + deploys config (all in one run)
-
Verify chat UI — test
llm.lab.alexpires.me/with OAuth2 login -
Verify API — test
llm.lab.alexpires.me/api/*(no auth yet, same as current direct access) -
Verify OpenWebUI still works — confirm dev-01 still connects to gpu-01:11434
Phase 2 — Switch to localhost + API key (after validation, decommission OpenWebUI)
Status: COMPLETE — All steps executed and verified.
- Update host vars — change
llama_server_bind: "127.0.0.1", add--api-keytollama_server_argv_extra✅ - Update firewall — remove port 11434 rules from
fw_allowed_ports✅ - Run llama_server playbook — restart container with new binding + API key ✅
- Run hardening playbook —
playbook_hardening_cis.yml --limit gpu-01.lab.alexpires.me -t ufwto apply firewall changes ✅ - Verify API —
llm.lab.alexpires.me/api/v1/chat/completionsreturns 401 without key, 200 with key ✅ - Decommission OpenWebUI — removed from
terraform/apps/dev-01/apps.tf✅ - Remove OpenWebUI DNS records — removed
open-webuiCNAME fromterraform/apps/dev-01/config.tfzones ✅ - Remove OpenWebUI locals — removed
open_webui_fqdn,open_webui_log_level,llm_proxyfrom config.tf ✅ - Remove
webui_secret_keyvariable — removed fromterraform/apps/dev-01/secrets.tf✅ - Remove
[openwebui]group — cleaned upinventory/hosts✅ - Run
tofu applyonterraform/apps/dev-01/— destroyed 8 OpenWebUI K8s resources (namespace, deployment, service, ingress, secret, service accounts) ✅
Phase 2 — Switch to localhost + API key (after validation, decommission OpenWebUI)
- Update host vars — change
llama_server_bind: "127.0.0.1", add--api-keytollama_server_argv_extra - Update firewall — remove port 11434 rules from
fw_allowed_ports - Run llama_server playbook — restart container with new binding + API key
- Run hardening playbook —
playbook_hardening_cis.yml --limit gpu-01.lab.alexpires.me -t ufwto apply firewall changes - Verify API — test
llm.lab.alexpires.me/api/*requires API key - Decommission OpenWebUI — remove Terraform module from
terraform/apps/dev-01/apps.tfandconfig.tf - Remove OpenWebUI DNS records — clean up
open-webuiCNAME interraform/apps/dev-01/config.tfzones - Remove
[openwebui]group — clean upinventory/hosts - Run
tofu applyonterraform/apps/dev-01/— destroys OpenWebUI K8s resources
Playbooks to run
Phase 1
COMPLETE — All steps executed and verified.
cd ansible
sectool exec ansible-playbook -u $ANSIBLE_USER playbook_nginx_proxy.yml # nginx + oauth2 + certbot (all-in-one)
Phase 2
COMPLETE — All steps executed and verified.
cd ansible
sectool exec ansible-playbook -u $ANSIBLE_USER playbook_llama_server.yml # container binding + API key
sectool exec ansible-playbook -u $ANSIBLE_USER playbook_hardening_cis.yml -t ufw # remove port 11434 rules
Then in terraform:
cd terraform/apps/dev-01
sectool exec tofu plan # review OpenWebUI destruction
sectool exec tofu apply --auto-approve # apply
Files to modify
| File | Phase | Change |
|---|---|---|
terraform/apps/dev-01/config.tf |
1 | Add llm A record to zones |
inventory/hosts |
1 | Add gpu-01 to [nginx] group, clean up duplicate [certbot] |
ansible/roles/nginx_proxy/templates/nginx-server.conf.j2 |
1 | Add no_auth per-site conditional (3 blocks) |
ansible/roles/llama_server/defaults/main.yml |
1 | Add llama_server_bind variable (default 0.0.0.0) |
ansible/roles/llama_server/tasks/el/main.yml |
1 | Use llama_server_bind in podman port mapping |
ansible/roles/nginx_proxy/tasks/nginx.yml |
1 | Add SELinux tasks (nginx_connect_any boolean + oauth2-proxy port to http_port_t) |
ansible/host_vars/gpu-01.lab.alexpires.me.yml |
1 | Add nginx sites (with extra_locations for /api/), OAuth2 creds, firewall rules |
ansible/host_vars/gpu-01.lab.alexpires.me.yml |
2 | Change llama_server_bind to 127.0.0.1, add --api-key to llama_server_argv_extra, remove port 11434 fw rules ✅ |
terraform/apps/dev-01/apps.tf |
2 | Remove module "open-webui" block ✅ |
terraform/apps/dev-01/config.tf |
2 | Remove open_webui_fqdn, open_webui_log_level, llm_proxy locals; remove open-webui DNS record from zones ✅ |
terraform/apps/dev-01/secrets.tf |
2 | Remove webui_secret_key variable ✅ |
inventory/hosts |
2 | Remove [openwebui] group ✅ |
Rollback plan
Phase 1 rollback (nginx proxy broken)
- Remove gpu-01 from
[nginx]group ininventory/hosts - Re-run hardening playbook on gpu-01 to clean up nginx/oauth2-proxy packages
- Revert
nginx-server.conf.j2template changes - llama.cpp continues running on
0.0.0.0:11434— no impact - OpenWebUI continues to work
Issues Encountered & Resolved
1. nginx copy module missing remote_src: true
File: ansible/roles/nginx_proxy/tasks/oauth2_proxy.yml:16
The copy module was trying to copy from the controller instead of the remote host. Added remote_src: true.
2. SELinux blocking nginx → oauth2-proxy / llama.cpp connections
Symptom: connect() to 127.0.0.1:4180 failed (13: Permission denied) in nginx error log
Fix:
- Set SELinux boolean
nginx_connect_anytotrue(persistent) - Added port 4180 to
http_port_tviasemanage port - Added both as Ansible tasks in
nginx.ymlfor persistence
3. Template no_auth attribute error
Symptom: object of type 'dict' has no attribute 'no_auth'
Fix: Changed {% if not item.no_auth %} to {% if not (item.no_auth | default(false) | bool) %} to handle missing attribute on dicts.
4. Playbook failed on model query after container restart (Phase 2)
Symptom: urlopen error [Errno 111] Connection refused when querying http://127.0.0.1:11434/models?reload=1
Cause: The playbook's "Query llama router model list" task ran before the container was fully started. The container did start correctly with 127.0.0.1:11434 binding and API key active.
Impact: Non-blocking — the container was running correctly; only the model list query task failed.
Known Warnings (non-blocking)
types_hash_bucket_size: 64— nginx warning, non-blocking- oauth2-proxy PKCE warning (
--code-challenge-method) — non-blocking, S256 is auto-negotiated
Rollback plan
Phase 1 rollback (nginx proxy broken)
- Revert
llama_server_bindto0.0.0.0in host vars - Remove
--api-keyfromllama_server_argv_extra - Re-add port 11434 firewall rules
- Re-run
playbook_llama_server.yml+playbook_hardening_cis.yml -t ufw— container restarts with old settings - OpenWebUI still works (llama.cpp is back on
0.0.0.0without API key) - Fix issues and re-attempt Phase 2
Phase 2 rollback (after OpenWebUI decommission)
- Restore
llama_server_bind: "0.0.0.0"in host vars - Remove
--api-keyfromllama_server_argv_extra - Re-add port 11434 firewall rules
- Restore
module "open-webui"interraform/apps/dev-01/apps.tf - Restore
open_webui_fqdn,open_webui_log_level,llm_proxylocals inconfig.tf - Restore
open-webuiDNS CNAME record inconfig.tfzones - Restore
webui_secret_keyvariable insecrets.tf - Re-add
[openwebui]group ininventory/hosts - Run
tofu applyto recreate OpenWebUI K8s resources