CI/CD with GitHub Actions
The classic way to deploy to Kubernetes from CI is to paste an admin kubeconfig into a repository secret and hope nobody ever leaks it. That credential is cluster-admin, lives for years, gets copied into forks and artifacts, and can’t be revoked without re-keying the cluster.
paasbox is built for a better pattern: the API key is the durable secret; kubeconfigs are disposable. Your workflow holds one revocable API key, uses it to fetch a short-lived admin kubeconfig at the start of each run — with a TTL sized to the job — deploys, and lets the credential expire on its own.
1. Create the API key
Section titled “1. Create the API key”In the console, create an API key (see teams & API keys) — it’s shown
once, looks like pbx_live_…, and can be revoked at any time. For CI, consider creating it from a
dedicated machine-user account, so pipeline actions are clearly attributable in the
activity log.
Store it as a repository secret, e.g. PAASBOX_API_KEY (GitHub → Settings → Secrets and
variables → Actions).
2. The workflow
Section titled “2. The workflow”A complete deploy workflow — fetch credential, apply manifests, verify the rollout:
name: deployon: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest env: TEAM: my-team CLUSTER: production steps: - uses: actions/checkout@v4
- name: Fetch a short-lived kubeconfig run: | curl -sf -X POST \ -H "Authorization: Token ${{ secrets.PAASBOX_API_KEY }}" \ -H "Content-Type: application/json" \ -d '{"ttlSeconds": 1800}' \ "https://console.paasbox.com/api/v1/teams/$TEAM/clusters/$CLUSTER/kubeconfig/" \ | jq -r .kubeconfig > "$RUNNER_TEMP/kubeconfig" chmod 600 "$RUNNER_TEMP/kubeconfig" echo "KUBECONFIG=$RUNNER_TEMP/kubeconfig" >> "$GITHUB_ENV"
- name: Deploy run: kubectl apply -k deploy/overlays/production
- name: Wait for the rollout run: kubectl -n myapp rollout status deploy/web --timeout=180sThirty minutes of validity (ttlSeconds: 1800) comfortably covers a deploy job; the response’s
expiresAt field lets longer pipelines renew mid-run if they ever need to. The
API reference documents the endpoint.
If your release includes an immutable migration Job — like the Django SaaS guide’s — the same delete-then-apply step slots in between fetch and rollout:
- name: Run migrations run: | kubectl -n myapp delete job myapp-migrate --ignore-not-found kubectl apply -f deploy/overlays/production/migrate-job.yaml kubectl -n myapp wait --for=condition=complete job/myapp-migrate --timeout=300sWhy short-lived beats long-lived
Section titled “Why short-lived beats long-lived”It’s worth being explicit about what this pattern buys you:
- Leaks age out. A kubeconfig accidentally printed to a log, cached in an artifact, or left on a runner is worthless within the hour — a stored one is cluster-admin until someone notices.
- Revocation is one click, and it’s not your cluster. Rotating a leaked API key is instant and self-service. Rotating a long-lived cluster credential means re-keying the cluster.
- Every issuance is audited. Each fetch lands in your team’s activity log with actor, time, and TTL — “what deployed at 02:00” has an answer.
- Offboarding stays clean. Remove a person, revoke their keys — nothing they ever exported keeps working past the TTL. The full story is in access your cluster.
Hardening checklist
Section titled “Hardening checklist”- Never upload the kubeconfig as an artifact and never
catit in a step — it’s short-lived, not public. - Size the TTL to the job, not to “just in case”. 30–60 minutes covers almost any deploy.
- One key per pipeline (staging vs. production, per repo) so revocation is surgical and the audit trail is legible.
- Pin image tags (
sha-…, notlatest) so what CI deploys is exactly what CI built.
The same pattern works in any CI system — GitLab CI, Forgejo Actions, Jenkins — since it’s just
curl and kubectl: hold the key, fetch fresh, deploy, let it expire.