CI/CD, Secret Management, and GitOps
- CI/CD, Secret Management, and GitOps
Getting code to production without handing out the keys
This page complements the comprehensive CI/CD reference at the repo root, which covers GitHub Actions, GitLab CI, and Jenkins in depth with worked examples. Read that page first for pipeline concepts, tool comparisons, and full workflow examples. This page adds the DevOps roadmap topics not covered there: secret management and GitOps.
ℹ️ The security thread tying these two topics together is credential exposure. Secret managers keep credentials out of source, logs, and process listings; GitOps’s pull model means the cluster reconciles itself from Git instead of handing deploy credentials to an external CI system. Both shrink the blast radius of a leaked secret.
CI/CD Overview
Continuous Integration and Continuous Deployment automate the path from committed code to running software. See the CI/CD reference for:
- Pipeline stages (build, test, security scan, package, deploy)
- GitHub Actions: workflow syntax, triggers, matrix builds, artifacts, caching
- GitLab CI:
.gitlab-ci.yml, stages, runners, environments - Jenkins: Jenkinsfile, declarative pipelines, plugins
Quick reference: additional CI tools
The CI/CD reference covers GitHub Actions, GitLab CI, and Jenkins. The DevOps roadmap also lists:
| Tool | Type | Key characteristic |
|---|---|---|
| Travis CI | Cloud | Pioneered .travis.yml; free tier deprecated |
| CircleCI | Cloud | Strong Docker/layer caching; orbs for reuse |
| Drone | Self-hosted | Lightweight; every step runs in a container |
| TeamCity | Self-hosted | JetBrains product; Kotlin DSL for pipelines |
Secret Management
Secrets are credentials that must not appear in source code or CI logs: API keys, database passwords, TLS private keys, SSH keys, and service account tokens. Secret management systems store these encrypted and provide controlled access.
Why not environment variables or .env files?
Environment variables in CI systems are better than hardcoded values, but they have limits:
- Secrets in environment variables can leak through process listings (
/proc/<pid>/environ), debug logs, or error messages - Rotating a secret requires updating every system that references it manually
- There is no audit trail of who accessed a secret
- Secrets stored in
.envfiles committed to a repository are permanently exposed, even after deletion
OpenBao
OpenBao is an open-source secrets management system — the OSI-licensed (MPL-2.0) Linux Foundation fork of HashiCorp Vault, adopted after Vault’s 2023 move to the BSL license (the same reasoning behind using OpenTofu over Terraform). It stores secrets encrypted, enforces access policies, issues short-lived dynamic credentials, and logs every access. The CLI is bao and the API is Vault-compatible, so these commands map one-to-one onto Vault.
# Install OpenBao (Debian/Ubuntu) — from the upstream .deb release asset
VER=2.5.5 # current release — see github.com/openbao/openbao/releases
curl -fsSLO "https://github.com/openbao/openbao/releases/download/v${VER}/bao_${VER}_linux_amd64.deb"
sudo apt install "./bao_${VER}_linux_amd64.deb"
bao version
Development server (local testing only)
bao server -dev # start in-memory dev server (NOT for production)
export BAO_ADDR='http://127.0.0.1:8200'
export BAO_TOKEN='root' # dev server token shown in startup output
bao status
Basic secret operations
# Key-value secrets engine (v2 — versioned)
bao secrets enable -path=secret kv-v2
# Write secrets
bao kv put secret/myapp/db \
username=appuser \
password=s3cr3t
# Read secrets
bao kv get secret/myapp/db
bao kv get -field=password secret/myapp/db # just the value
# List secrets
bao kv list secret/myapp
# Update a secret (creates a new version)
bao kv patch secret/myapp/db password=newpassword
# Delete (soft delete — versions retained)
bao kv delete secret/myapp/db
# Destroy a specific version permanently
bao kv destroy -versions=1 secret/myapp/db
Policies
# policy/myapp-read.hcl — allow read-only access to myapp secrets
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
path "secret/metadata/myapp/*" {
capabilities = ["list"]
}
bao policy write myapp-read policy/myapp-read.hcl
bao policy list
bao policy read myapp-read
Dynamic credentials (database example)
Dynamic credentials are generated on demand and expire automatically — the database creates a temporary user with a time-limited password.
bao secrets enable database
bao write database/config/mydb \
plugin_name=postgresql-database-plugin \
allowed_roles="app-role" \
connection_url="postgresql://:@db:5432/mydb" \
username="bao-admin" \
password="bao-admin-pass"
bao write database/roles/app-role \
db_name=mydb \
creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"\";" \
default_ttl="1h" \
max_ttl="24h"
# Application requests credentials
bao read database/creds/app-role
# Returns: username=v-app-role-xyz123, password=A1b2C3d4, lease_duration=1h
Python integration
import hvac # the Vault API client — works unchanged against OpenBao
import os
client = hvac.Client(
url=os.environ['BAO_ADDR'],
token=os.environ['BAO_TOKEN'],
)
# Read a static secret
secret = client.secrets.kv.v2.read_secret_version(
path='myapp/db',
mount_point='secret',
)
db_password = secret['data']['data']['password']
# Request dynamic database credentials
creds = client.secrets.database.generate_credentials(name='app-role')
username = creds['data']['username']
password = creds['data']['password']
Sealed Secrets (Kubernetes)
Bitnami Sealed Secrets allows storing Kubernetes secrets as encrypted SealedSecret objects in Git. Only the controller running in the cluster can decrypt them, so the encrypted form is safe to commit.
# Install kubeseal CLI
curl -L https://github.com/bitnami-labs/sealed-secrets/releases/latest/download/kubeseal-linux-amd64 \
-o /usr/local/bin/kubeseal
chmod +x /usr/local/bin/kubeseal
# Seal a secret
kubectl create secret generic db-credentials \
--from-literal=username=appuser \
--from-literal=password=s3cr3t \
--dry-run=client -o yaml \
| kubeseal --format yaml > sealed-db-credentials.yaml
# This file is safe to commit to Git
git add sealed-db-credentials.yaml
git commit -m "add sealed database credentials"
# Apply to cluster (controller decrypts and creates the real Secret)
kubectl apply -f sealed-db-credentials.yaml
SOPs (Secrets OPerationS)
SOPS is a file encryption tool from Mozilla. It encrypts individual values within YAML, JSON, and .env files using PGP keys, AWS KMS, GCP KMS, or Azure Key Vault — leaving keys visible but values encrypted.
# Install
curl -L https://github.com/getsops/sops/releases/latest/download/sops-v3.9.0.linux.amd64 \
-o /usr/local/bin/sops
chmod +x /usr/local/bin/sops
# Create a .sops.yaml config
cat > .sops.yaml << 'EOF'
creation_rules:
- path_regex: .*\.secret\.yaml$
pgp: FINGERPRINT_HERE
- path_regex: secrets/.*
aws_kms: arn:aws:kms:us-east-1:123456789012:key/abc-123
EOF
# Encrypt a secrets file
sops -e secrets.yaml > secrets.enc.yaml
# Decrypt and view
sops -d secrets.enc.yaml
# Edit in place (decrypts, opens editor, re-encrypts on save)
sops secrets.enc.yaml
Cloud provider secret services
| Provider | Service | Notes |
|---|---|---|
| AWS | Secrets Manager | Automatic rotation; charges per secret per month |
| AWS | Parameter Store (SSM) | Free tier; simpler; no automatic rotation |
| GCP | Secret Manager | Per-access pricing; IAM integration |
| Azure | Key Vault | Stores secrets, keys, and certificates |
# Python: read from AWS Secrets Manager
import boto3
import json
def get_secret(secret_name, region='us-east-1'):
client = boto3.client('secretsmanager', region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
creds = get_secret('prod/myapp/db')
print(creds['username']) # appuser
GitOps
GitOps is an operational model where the desired state of a system is stored entirely in Git, and an automated agent continuously reconciles the actual state to match it. Coined by Weaveworks in 2017, it applies the developer workflow (pull requests, code review, audit trail) to operations.
GitOps principles
- Declarative — the entire system state is described declaratively (Kubernetes manifests, Helm charts)
- Versioned and immutable — the desired state is stored in Git, providing history and rollback
- Pulled automatically — an agent inside the cluster pulls changes from Git (not pushed from CI)
- Continuously reconciled — the agent detects drift and corrects it
The pull-based model is more secure than push-based CI deployments: the cluster does not need external CI systems to have credentials that can modify it.
ArgoCD
ArgoCD is the most widely used GitOps controller for Kubernetes. It watches a Git repository and continuously reconciles the cluster state to match what is declared in that repo.
# Install ArgoCD into a Kubernetes cluster
kubectl create namespace argocd
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods to be ready
kubectl wait --for=condition=available --timeout=300s \
deployment/argocd-server -n argocd
# Install ArgoCD CLI
curl -sSL -o /usr/local/bin/argocd \
https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x /usr/local/bin/argocd
# Port-forward to access the UI
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
# Log in
argocd login localhost:8080 --username admin \
--password $(kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d)
Defining an application
# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp-manifests.git
targetRevision: main
path: kubernetes/production
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true # delete resources removed from Git
selfHeal: true # revert manual changes in cluster
syncOptions:
- CreateNamespace=true
kubectl apply -f argocd-app.yaml
argocd app list # list applications
argocd app get myapp # show sync status
argocd app sync myapp # trigger manual sync
argocd app history myapp # deployment history
argocd app rollback myapp 2 # roll back to revision 2
FluxCD
FluxCD (Flux v2) is an alternative GitOps toolkit. It is more modular than ArgoCD — composed of separate controllers for Git syncing, Helm releases, image automation, and notifications.
# Install Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash
# Bootstrap Flux into a cluster (connects to GitHub repo)
flux bootstrap github \
--owner=myorg \
--repository=fleet-infra \
--branch=main \
--path=clusters/production \
--personal
# Check Flux components
flux check
flux get all # show all Flux resources
Flux GitRepository and Kustomization
# flux-source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: myapp
namespace: flux-system
spec:
interval: 1m
url: https://github.com/myorg/myapp-manifests
ref:
branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
path: ./kubernetes/production
prune: true
sourceRef:
kind: GitRepository
name: myapp
targetNamespace: myapp
kubectl apply -f flux-source.yaml
flux get kustomizations # show reconciliation status
flux reconcile kustomization myapp # force immediate reconciliation
flux logs # show Flux controller logs
ArgoCD vs FluxCD
| Aspect | ArgoCD | FluxCD |
|---|---|---|
| UI | Built-in web UI | Third-party (Weave GitOps) |
| Architecture | Single controller | Modular toolkit |
| Helm support | Built-in | Helm controller |
| Multi-cluster | ApplicationSet CRD | Multi-tenancy via namespaces |
| Image auto-update | Argo CD Image Updater | Flux Image Automation |
| RBAC | Fine-grained project RBAC | Kubernetes RBAC only |
Both are CNCF graduated projects. ArgoCD is often preferred when a visual UI matters to the team; FluxCD when a pure-Kubernetes, operator-composable approach is preferred.
GitOps repository structure
fleet-infra/ # GitOps repo
├── clusters/
│ ├── production/
│ │ ├── flux-system/ # Flux bootstrap manifests
│ │ └── apps.yaml # Kustomization pointing to apps/production
│ └── staging/
│ └── apps.yaml
└── apps/
├── base/ # shared manifests (Deployment, Service)
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
├── production/
│ ├── kustomization.yaml # patches: replicas=5, image tag
│ └── patches.yaml
└── staging/
├── kustomization.yaml # patches: replicas=1
└── patches.yaml
Deployment workflow with GitOps:
# Developer: update image tag in the manifests repo
cd apps/production
sed -i 's/image: myapp:.*/image: myapp:v1.2.3/' patches.yaml
git add patches.yaml
git commit -m "deploy myapp v1.2.3 to production"
git push
# ArgoCD/Flux detects the commit and reconciles automatically
# No CI pipeline needs cluster credentials
Key takeaways
- Secrets never belong in source, CI logs, or
.envfiles in Git — even deleted, a committed secret lives forever in history. Use a manager that encrypts at rest, enforces access policy, and logs every access. - OpenBao (the open-source Vault fork) is the workhorse: a versioned KV engine, HCL policies, and dynamic credentials that the database mints on demand and that expire automatically — far better than a long-lived shared password.
- For Kubernetes-native workflows, Sealed Secrets (encrypt so the ciphertext is safe to commit) and SOPS (encrypt values in-place in YAML/JSON via PGP/KMS) let secrets live in Git safely; cloud providers offer managed services (AWS Secrets Manager/SSM, GCP Secret Manager, Azure Key Vault).
- GitOps makes Git the single source of truth for system state, with an in-cluster agent that pulls and continuously reconciles — giving you rollback, drift correction, and an audit trail for free.
- The pull model is more secure than push: the cluster reconciles itself, so no external CI system needs standing credentials that can modify it. ArgoCD (UI-first) and FluxCD (modular toolkit) are the two CNCF-graduated controllers.
References
- OpenBao documentation. https://openbao.org/docs/
- Bitnami Sealed Secrets. https://github.com/bitnami-labs/sealed-secrets
- SOPS — Secrets OPerationS. https://github.com/getsops/sops
- Argo CD documentation. https://argo-cd.readthedocs.io/
- Flux documentation. https://fluxcd.io/flux/
- OpenGitOps — principles (CNCF). https://opengitops.dev/
Related course pages: CI/CD reference · OpenBao · Infrastructure as Code
🛠️ Maintenance note: the install snippets pin versions (
sops-v3.9.0, OpenBao2.5.5,latestrelease URLs) that drift — re-verify each term and bump the OpenBao tag from its releases page. This course uses OpenBao (the OSI-licensed Linux Foundation fork) rather than HashiCorp Vault, which moved to the BSL license in 2023 — thebaoCLI and API stay Vault-compatible. ArgoCD and Flux both reached CNCF graduated status; their bootstrap commands and CRDapiVersions evolve between releases.