Skip to main content

New Project — Secrets & CI/CD Variables Setup

Audience: Any engineer (or Claude Code session) bootstrapping a new project that needs a GitLab CI/CD pipeline deploying to Kubernetes.

This guide covers the exact commands to run from each of the three environments we use — Windows (Git Bash / PowerShell), WSL2, and Claude Desktop — to:

  1. Create an Infisical project and seed application secrets
  2. Set the required GitLab CI/CD variables (KUBE_CONFIG)
  3. Verify the pipeline can authenticate to the registry and cluster

Nothing is ever committed to git. All secrets live in Infisical and are synced to K8s via InfisicalSecret CR.


Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│ SECRET SOURCES │
│ │
│ Infisical (anshin-knowledge project) │
│ └── KNOWLEDGE_JWT_SECRET, LDAP_URL, LDAP_BASE_DN, … │
│ │ │
│ ▼ InfisicalSecret CR (k8s/base/infisical-secret.yaml)│
│ K8s Secret "knowledge-secrets" (namespace: knowledge) │
│ │ │
│ ▼ envFrom: [{secretRef: {name: knowledge-secrets}}] │
│ Pod ENV VARS ← your app reads process.env.* │
│ │
│ GitLab CI/CD Variables (project-level) │
│ └── KUBE_CONFIG → used by kubectl in deploy job │
│ └── CI_REGISTRY_USER / CI_REGISTRY_PASSWORD (auto-set) │
└─────────────────────────────────────────────────────────────┘

Prerequisites — Credentials You Need

These credentials are already stored in the following locations. You do not need to generate new ones for each project.

CredentialWhere it livesUsed for
INFISICAL_CLIENT_ID~/.claude/settings.jsonmcpServers.infisical.envInfisical API auth
INFISICAL_CLIENT_SECRETsameInfisical API auth
GITLAB_TOKEN~/.claude/settings.jsonenv.GITLAB_TOKENGitLab API
KUBE_CONFIG (base64)GitLab project 90 (superpai) CI/CD variablekubectl access

The KUBE_CONFIG is shared across all projects. Copy it from superpai — never regenerate it unless the cluster cert changes.


Environment 1 — Windows (Git Bash / Claude Code terminal)

This is the primary environment for Claude Code sessions on the Windows workstation (spl-hp-eb850-bl).

Step 1 — Get an Infisical access token

INFISICAL_TOKEN=$(curl -sk -X POST \
"https://infisical.svcs.anshinhealth.net/api/v1/auth/universal-auth/login" \
-H "Content-Type: application/json" \
-d "{\"clientId\":\"${INFISICAL_CLIENT_ID}\",\"clientSecret\":\"${INFISICAL_CLIENT_SECRET}\"}" \
| python -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")

echo "Token acquired: ${INFISICAL_TOKEN:0:20}..."

Note: INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET are in your Claude Code settings.json env block and are available automatically in all Claude Code Bash sessions.

Step 2 — Create the Infisical project

ORG_ID="6ee19673-90fe-4c2c-8e5f-1ebd9a9f7cf1" # Anshin Health org — fixed

NEW_PROJECT=$(curl -sk -X POST \
"https://infisical.svcs.anshinhealth.net/api/v2/workspace" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"projectName\":\"<your-project-name>\",\"slug\":\"<your-project-slug>\",\"organizationId\":\"${ORG_ID}\"}" \
| python -c "import sys,json; d=json.load(sys.stdin); print(d['project']['id'])")

echo "Project ID: $NEW_PROJECT"

Replace <your-project-name> and <your-project-slug> with your project identifier (e.g. anshin-knowledge / anshin-knowledge).

Step 3 — Add application secrets

For each secret your app needs:

add_secret() {
local KEY=$1
local VALUE=$2
curl -sk -X POST \
"https://infisical.svcs.anshinhealth.net/api/v3/secrets/raw/${KEY}" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"workspaceId\":\"${NEW_PROJECT}\",\"environment\":\"prod\",\"secretPath\":\"/\",\"secretValue\":\"${VALUE}\"}" \
| python -c "import sys,json; d=json.load(sys.stdin); print(d.get('secret',{}).get('secretKey','ERR'))"
}

# Example: secrets for a Bun/Hono server
add_secret "JWT_SECRET" "$(python -c 'import secrets; print(secrets.token_hex(32))')"
add_secret "NODE_ENV" "production"
add_secret "PORT" "3000"
# Add your app-specific secrets here
add_secret "DATABASE_URL" "your-db-url"

Step 4 — Verify secrets exist

curl -sk \
"https://infisical.svcs.anshinhealth.net/api/v3/secrets/raw?workspaceSlug=<your-project-slug>&environment=prod&secretPath=/" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
| python -c "import sys,json; [print(s['secretKey']) for s in json.load(sys.stdin).get('secrets',[])]"

Step 5 — Create the GitLab repo (if not done yet)

# Create repo via API
curl -sk -X POST "https://gitlab.anshinhealth.net/api/v4/projects" \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"<your-repo-name>\",\"namespace_id\":6,\"initialize_with_readme\":false}" \
| python -c "import sys,json; d=json.load(sys.stdin); print(f'Project ID: {d[\"id\"]} | URL: {d[\"web_url\"]}')"

namespace_id: 6 = the engineering group.

Step 6 — Set KUBE_CONFIG on the new GitLab project

# Grab KUBE_CONFIG from superpai (project 90) and copy to your new project
PROJECT_ID=<your-gitlab-project-id>

KUBE_CONFIG_VAL=$(curl -sk \
"https://gitlab.anshinhealth.net/api/v4/projects/90/variables/KUBE_CONFIG" \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
| python -c "import sys,json; print(json.load(sys.stdin)['value'])")

curl -sk -X POST \
"https://gitlab.anshinhealth.net/api/v4/projects/${PROJECT_ID}/variables" \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"key\":\"KUBE_CONFIG\",\"value\":\"${KUBE_CONFIG_VAL}\",\"protected\":false,\"masked\":false}" \
| python -c "import sys,json; d=json.load(sys.stdin); print('Set: '+d.get('key','ERR: '+str(d)[:80]))"

Step 7 — Configure SSL for internal GitLab

Git Credential Manager doesn't know about your internal CA. Disable TLS verification for just this host:

cd "C:\projects\<your-repo>"
git config http.https://gitlab.anshinhealth.net.sslVerify false

Set the authenticated remote URL using your PAT:

git remote set-url origin "https://bryan.lee:${GITLAB_TOKEN}@gitlab.anshinhealth.net/engineering/<your-repo>.git"

Step 8 — Push and verify pipeline triggers

git push -u origin dev
# Watch pipelines:
# https://gitlab.anshinhealth.net/engineering/<your-repo>/-/pipelines

Environment 2 — WSL2 (Ubuntu)

WSL2 is used for Linux-native tooling (kubectl, helm, ansible). The credentials live in different paths than Windows.

Key path differences

ItemWindows pathWSL2 path
Claude settingsC:\Users\BryanLee\.claude\settings.json/home/bryanlee/.claude/settings.json
kubeconfigC:\Users\BryanLee\.kube\config/home/bryanlee/.kube/config
ProjectsC:\projects\/home/bryanlee/projects/

Accessing Windows files from WSL2

The Git Bash shell on Windows intercepts Linux-style paths. Use this pattern to access WSL2 files from Windows:

# From Windows Git Bash / Claude Code — read a WSL2 file
powershell -Command "wsl -d Ubuntu -- cat /home/bryanlee/.claude/settings.json"

# Copy file from WSL2 to Windows
powershell -Command "wsl -d Ubuntu -- cat /home/bryanlee/path/file" > "C:\destination\file"

Getting credentials in WSL2

Open a WSL2 terminal directly, or run via powershell:

# Extract Infisical credentials from settings.json (WSL2 side)
INFISICAL_CLIENT_ID=$(cat /home/bryanlee/.claude/settings.json | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d['mcpServers']['infisical']['env']['INFISICAL_CLIENT_ID'])")

INFISICAL_CLIENT_SECRET=$(cat /home/bryanlee/.claude/settings.json | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d['mcpServers']['infisical']['env']['INFISICAL_CLIENT_SECRET'])")

GITLAB_TOKEN=$(cat /home/bryanlee/.claude/settings.json | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d['env']['GITLAB_TOKEN'])")

Steps 1–6 — Same as Windows

Run the exact same curl commands from Steps 1–6 above in your WSL2 terminal. The API endpoints are all HTTPS and accessible from both environments.

WSL2-specific: kubectl configuration

In WSL2, kubectl reads from ~/.kube/config. To set it up from the GitLab CI variable:

# Decode and write the kubeconfig
KUBE_CONFIG_VAL=$(curl -sk \
"https://gitlab.anshinhealth.net/api/v4/projects/90/variables/KUBE_CONFIG" \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['value'])")

mkdir -p ~/.kube
echo "$KUBE_CONFIG_VAL" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config

# Verify
kubectl get nodes

WSL2-specific: git remote SSL

WSL2's git uses the system CA bundle. Add the internal CA:

# Option A: skip TLS just for this repo (same as Windows)
cd ~/projects/<your-repo>
git config http.https://gitlab.anshinhealth.net.sslVerify false

# Option B: install the internal CA permanently (if you have it)
sudo cp /path/to/anshin-internal-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

Environment 3 — Claude Desktop (macOS / Windows app)

Claude Desktop runs MCP servers but does not have a terminal shell. You cannot run bash commands directly. There are two approaches:

Claude Desktop has access to the Infisical MCP server configured in settings.json. Use MCP tool calls directly in the chat to manage secrets.

Creating a secret via MCP in Claude Desktop:

Ask Claude Desktop:

"Using the Infisical MCP server, create a new project called my-project in the Anshin org, then add these secrets to the prod environment: JWT_SECRET=<value>, NODE_ENV=production"

Claude Desktop will call infisical_create_workspace and infisical_create_secret MCP tools without needing a terminal.

Limitations of Claude Desktop:

  • Cannot run git push or kubectl without a connected terminal MCP server
  • Cannot access the Windows filesystem directly (no Read/Write tools unless shell MCP is configured)
  • CI/CD variable setup via GitLab API must be done through the chat using WebFetch (POST requests)

Approach B — Claude Desktop → delegate to terminal session

From Claude Desktop, generate the exact bash commands and then execute them in a separate Claude Code session or terminal:

  1. In Claude Desktop: generate all the curl commands with correct values filled in
  2. Copy to a Claude Code terminal session (Git Bash on Windows or WSL2)
  3. Execute there

This is the safest pattern when Claude Desktop is being used for planning but Claude Code handles execution.

Checking Claude Desktop's available MCP servers

Claude Desktop loads MCP servers from its own config file. On Windows:

C:\Users\BryanLee\AppData\Roaming\Claude\claude_desktop_config.json

Ensure infisical is listed there if you want to use MCP tools directly from Desktop.


Full New-Project Checklist

Use this checklist every time you start a new deployable project:

Phase 1 — Repository

  • Create GitLab repo in engineering group (namespace_id: 6)
  • Initialize with dev branch (CI/CD trigger) and main branch (stable)
  • Set git config http.https://gitlab.anshinhealth.net.sslVerify false locally
  • Set PAT-authenticated remote: https://bryan.lee:${GITLAB_TOKEN}@gitlab.anshinhealth.net/...

Phase 2 — Infisical

  • Authenticate: POST /api/v1/auth/universal-auth/login → save accessToken
  • Create project: POST /api/v2/workspace with org ID 6ee19673-...
  • Add NODE_ENV=production
  • Add PORT=<port>
  • Add all app-specific secrets (DB credentials, JWT secrets, API keys, etc.)
  • Verify: GET /api/v3/secrets/raw?workspaceSlug=<slug>&environment=prod&secretPath=/

Phase 3 — GitLab CI/CD Variables

  • Set KUBE_CONFIG — copy from project 90 (superpai), never regenerate
  • Confirm CI_REGISTRY_USER / CI_REGISTRY_PASSWORD are auto-provided (no action needed)
  • Set any additional pipeline-specific variables (e.g. HELM_CHART_VERSION)

Phase 4 — K8s Manifests

  • k8s/base/namespace.yaml — create the namespace
  • k8s/base/infisical-secret.yamlInfisicalSecret CR pointing to your Infisical project slug
  • k8s/base/deployment.yamlenvFrom: [{secretRef: {name: <project>-secrets}}]
  • k8s/base/pvc.yaml — persistent storage if needed (storageClass: nfs)
  • k8s/base/service.yaml + ingress.yaml — ClusterIP + Caddy ingress

Phase 5 — Pipeline

  • .gitlab-ci.yml uses Kaniko (not docker:dind)
  • Runner tags: orchestrate-build (build), orchestrate-dev (deploy/verify)
  • Build job: Kaniko executor with CI_REGISTRY destination
  • Deploy job: kubectl apply + rollout status with 3-minute timeout
  • Verify job: health check loop against the live URL

Phase 6 — Push & Monitor

  • git push origin dev — triggers pipeline
  • Monitor: https://gitlab.anshinhealth.net/engineering/<repo>/-/pipelines
  • After first successful deploy: run init-admin or equivalent seed script
  • Add any post-deploy secrets (e.g. MCP_TOKEN after first admin login) to Infisical

Reference: Fixed Values

These values never change — hardcode them in your scripts:

ConstantValue
Infisical base URLhttps://infisical.svcs.anshinhealth.net
Infisical org ID6ee19673-90fe-4c2c-8e5f-1ebd9a9f7cf1
GitLab base URLhttps://gitlab.anshinhealth.net
GitLab engineering group ID6
Source of KUBE_CONFIGGitLab project ID 90 (superpai), variable KUBE_CONFIG
K8s storage classnfs (QNAP NFS at 10.10.96.31)
Container registryregistry.anshinhealth.net (no port — GitLab integrated)
Infisical K8s auth secretinfisical-universal-auth in namespace infisical

Troubleshooting

"Secret already exists" error from Infisical

# Use PATCH instead of POST to update an existing secret
curl -sk -X PATCH \
"https://infisical.svcs.anshinhealth.net/api/v3/secrets/raw/${KEY}" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"workspaceId\":\"${WS_ID}\",\"environment\":\"prod\",\"secretPath\":\"/\",\"secretValue\":\"${NEW_VALUE}\"}"

"TLS certificate verification disabled" warning

Expected for internal GitLab. The git config http.https://gitlab.anshinhealth.net.sslVerify false setting is correct — it scopes the bypass to only this host.

Pipeline stuck on "pending"

The runner has a single build slot (resource_group: kaniko-build-1). If another pipeline is running, it queues. Check: https://gitlab.anshinhealth.net/engineering/<repo>/-/pipelines

Do not push another commit while a pipeline is running — it will queue a second build.

"missing OAuth configuration" from Git Credential Manager

The internal GitLab uses a self-signed cert that GCM can't verify. Solution: always embed the PAT in the remote URL:

git remote set-url origin "https://bryan.lee:${GITLAB_TOKEN}@gitlab.anshinhealth.net/engineering/<repo>.git"

InfisicalSecret CR not syncing

Check the operator logs:

kubectl logs -n infisical deployment/infisical-operator-controller-manager --tail=50

Common cause: the infisical-universal-auth secret in the infisical namespace has expired credentials. Re-run the Infisical login flow and update the secret.

kubectl "connection refused" or "certificate signed by unknown authority"

The KUBE_CONFIG is for the K8s API at https://10.10.97.1:6443. If you're running kubectl from outside the lab network (no VPN), it will fail. Use Claude Code on the Windows workstation which is on the same subnet.