title: "Secrets Encryption, LiteLLM Config & Smoke Test"
type: ticket
subtype: infrastructure
purpose: "Encrypt the secrets file with SOPS+age, update litellm_config.yaml for Vertex AI, run full runtime smoke test."
Three tasks in sequence:
A. Set up SOPS + age encryption and encrypt infrastructure_secrets.yaml → infrastructure_secrets.enc.yaml
B. Update litellm_config.yaml to correctly reference Google Vertex AI credentials with the new Gemini model names
C. Run a full runtime smoke test to verify the DEVXIO portal renders and all services respond
infrastructure_secrets.yaml must NEVER be committed to Gitinfrastructure_secrets.enc.yaml MUST be committed to Gitinfrastructure_secrets.yaml (or a separate local-only file) — never committedinfrastructure.yaml — it was already updated and pushed by the Founderage-keygen has generated a keypair. Public key is stored in STRUXIO_OS/01_state/.sops.yaml. Private key is stored locally only.sops --encrypt produces a valid infrastructure_secrets.enc.yaml that is committed and pushed to Git.sops --decrypt infrastructure_secrets.enc.yaml correctly recovers the original secrets file (test round-trip).litellm_config.yaml contains model entries for gemini-3.1-pro, gemini-3-flash, and gemini-3.1-flash-lite with correct vertex_ai/ prefixes and credential file path.curl -s -o /dev/null -w "%{http_code}" http://89.167.96.154:3002 returns 200.docker ps shows all expected containers running.curl http://localhost:4002/health (DEV port).# On Hetzner (Ubuntu 24.04)
# Install age
sudo apt-get update && sudo apt-get install -y age
# Install SOPS
curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
sudo mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
# Verify
age --version
sops --version
# Generate keypair
age-keygen -o ~/age-key.txt
# Display public key (we need this for SOPS config)
grep "public key:" ~/age-key.txt
IMPORTANT: ~/age-key.txt contains the private key. It stays on the server only. Never commit it. Note the public key output — it looks like age1xxxxx....
FILE CREATE: STRUXIO_OS/01_state/.sops.yaml
creation_rules:
- path_regex: infrastructure_secrets\.yaml$
age: >-
PUBLIC_KEY_FROM_STEP_B
Replace PUBLIC_KEY_FROM_STEP_B with the actual age1... public key from Sub-Task B.
cd ~/STRUXIO_Workspace/STRUXIO_OS/01_state
# Set the age key location for SOPS
export SOPS_AGE_KEY_FILE=~/age-key.txt
# Encrypt
sops --encrypt infrastructure_secrets.yaml > infrastructure_secrets.enc.yaml
# Verify round-trip
sops --decrypt infrastructure_secrets.enc.yaml > /tmp/secrets_test.yaml
diff infrastructure_secrets.yaml /tmp/secrets_test.yaml
# Expected: no output (files are identical)
rm /tmp/secrets_test.yaml
echo "Encryption round-trip verified."
FILE EDIT: STRUXIO_OS/02_infra/litellm_config.yaml
Ensure the model_list contains these entries (add or replace as needed). Keep any existing Anthropic/OpenAI entries intact:
model_list:
# ── Anthropic Models ──────────────────────────────────────────────────────
- model_name: struxio-active
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-sonnet-4-6
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
# ── Google Vertex AI Models (Gemini) ──────────────────────────────────────
# These use service account auth via a JSON credential file.
# The file is mounted into the container at /app/secrets/ via docker-compose.
- model_name: gemini-3.1-pro
litellm_params:
model: vertex_ai/gemini-3.1-pro-preview
vertex_project: "struxio-devxio-core"
vertex_location: "us-central1"
vertex_credentials: "/app/secrets/struxio-vertex-service-account.json"
- model_name: gemini-3-flash
litellm_params:
model: vertex_ai/gemini-3-flash
vertex_project: "struxio-devxio-core"
vertex_location: "us-central1"
vertex_credentials: "/app/secrets/struxio-vertex-service-account.json"
- model_name: gemini-3.1-flash-lite
litellm_params:
model: vertex_ai/gemini-3.1-flash-lite-preview
vertex_project: "struxio-devxio-core"
vertex_location: "us-central1"
vertex_credentials: "/app/secrets/struxio-vertex-service-account.json"
# ── OpenAI (Whisper Only) ─────────────────────────────────────────────────
- model_name: whisper-1
litellm_params:
model: openai/whisper-1
api_key: os.environ/OPENAI_API_KEY
FILE EDIT: STRUXIO_OS/02_infra/docker-compose.yml
In the LiteLLM service block, add a volume mount for the service account JSON:
devxio-litellm:
# ... existing config ...
volumes:
- ../01_state/struxio-vertex-service-account.json:/app/secrets/struxio-vertex-service-account.json:ro
# ... any existing volumes ...
The :ro flag makes it read-only inside the container.
cd ~/STRUXIO_Workspace/STRUXIO_OS
# Rebuild all containers
docker compose -f 02_infra/docker-compose.yml down
docker compose -f 02_infra/docker-compose.yml up -d --build
# Wait for startup
sleep 30
# ── Smoke Test Battery ──────────────────────────────────────────────────────
echo "=== Test 1: Frontend HTTP ==="
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://89.167.96.154:3002
echo "=== Test 2: Frontend renders HTML ==="
DIVCOUNT=$(curl -s http://89.167.96.154:3002 | grep -c "<div")
echo "Div count: $DIVCOUNT"
echo "=== Test 3: Docker containers ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo "=== Test 4: LiteLLM health ==="
curl -s http://localhost:4002/health || echo "LiteLLM health check failed"
echo "=== Test 5: FastAPI bridge ==="
curl -s http://89.167.96.154:8002/health || curl -s http://89.167.96.154:8002/docs || echo "Bridge health check failed — try /docs endpoint"
echo "=== Test 6: WebSocket handshake ==="
python3 -c "
import asyncio
try:
import websockets
except ImportError:
import subprocess, sys
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'websockets', '-q'])
import websockets
async def test():
try:
async with websockets.connect('ws://89.167.96.154:8002/ws/execute') as ws:
print('WS CONNECTED OK')
except Exception as e:
print(f'WS FAILED: {e}')
asyncio.run(test())
"
echo "=== Test 7: LiteLLM model list ==="
curl -s http://localhost:4002/model/info | python3 -m json.tool 2>/dev/null | head -30 || echo "Model info endpoint not available"
Paste ALL raw output into the LOG file. No interpretation — just evidence.
cd ~/STRUXIO_Workspace/STRUXIO_OS
# Stage the encrypted file and configs (NOT the unencrypted secrets)
git add 01_state/infrastructure_secrets.enc.yaml
git add 01_state/.sops.yaml
git add 02_infra/litellm_config.yaml
git add 02_infra/docker-compose.yml
# Verify infrastructure_secrets.yaml is NOT staged
git diff --cached --name-only | grep -v "enc" | grep "secrets" && echo "⚠️ UNENCRYPTED SECRETS STAGED — ABORT" && exit 1
git commit -m "feat(infra): SOPS encryption, Vertex AI models in LiteLLM, credential mount"
git push origin main
STRUXIO_OS/01_state/.sops.yaml (NEW)
STRUXIO_OS/01_state/infrastructure_secrets.enc.yaml (UPDATED — encrypted)
STRUXIO_OS/02_infra/litellm_config.yaml (UPDATED — Gemini models added)
STRUXIO_OS/02_infra/docker-compose.yml (UPDATED — volume mount for Vertex JSON)
If LiteLLM fails to start after the config change:
git checkout HEAD -- 02_infra/litellm_config.yamldocker compose -f 02_infra/docker-compose.yml restart devxio-litellmdocker logs devxio-litellm --tail 50STRUXIO.ai // Confidential & Proprietary // Generated by Claude Opus 4.6 (Principal Architect) // © 2026