94 Commits

Author SHA1 Message Date
73376a0a7d vault migration 2026-04-30 22:38:33 +02:00
2e09a2d404 ign diff 2026-04-30 19:08:24 +02:00
9e9254a466 auto flow 2026-04-30 18:24:34 +02:00
539217c3f2 subst 2026-04-30 18:16:25 +02:00
80cf435486 test2 2026-04-30 18:11:48 +02:00
0d7980d105 env secret 2026-04-30 18:05:09 +02:00
f280596ddb debug 2026-04-30 18:00:08 +02:00
65dc795cd6 idp hint 2026-04-30 15:50:23 +02:00
237dc0ff90 new idp 2026-04-30 15:34:58 +02:00
788cc8f4f4 tenantId 2026-04-30 15:19:37 +02:00
4def4d2ed7 cli enable 2026-04-30 15:08:20 +02:00
7d1e2d4665 broker alias 2026-04-30 14:49:38 +02:00
417185d567 idp auto config
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 14:37:13 +02:00
03e60a3512 kc syncer 2026-04-30 11:45:30 +02:00
2135580210 kc script 2026-04-29 14:42:27 +02:00
37a38a1179 feedback app
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 14:22:02 +02:00
4ca9039686 kpolicy 2026-04-29 12:54:07 +02:00
6a9eadbde8 vault ignore diffs 2026-04-29 12:50:10 +02:00
f19f7c9237 icon 2026-04-29 12:07:04 +02:00
5a459d486e dbunk-demo 2026-04-29 10:53:35 +02:00
31fb476a78 row 2026-04-29 10:06:02 +02:00
a088425b70 homepage config 2026-04-29 10:04:20 +02:00
b3b3edf82c no header 2026-04-28 23:03:15 +02:00
308755a4b3 layout 2026-04-28 23:02:13 +02:00
db6afaf180 vault
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 22:44:57 +02:00
5a2f9a1b88 Update infra/values/base/keycloak-values.yaml
Signed-off-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
2026-04-28 19:27:38 +00:00
1c6f18b67c homepage 2026-04-28 20:38:59 +02:00
7132f5000e docs 2026-04-27 20:35:27 +02:00
b4100bd456 mm ns 2026-04-27 20:16:06 +02:00
fff117a500 ns 2026-04-27 17:40:46 +02:00
03c75fc4cd mm ns 2026-04-27 17:40:05 +02:00
df73c4bdc0 mm sync pol 2026-04-27 17:37:54 +02:00
6a7de704f2 enterprise-apps 2026-04-27 17:34:43 +02:00
be8bbd2c12 aksapps 2026-04-27 17:33:47 +02:00
c469ab44b0 ent apps 2026-04-27 17:28:48 +02:00
290c8b91f8 db pass 2026-04-27 14:05:38 +02:00
a776bae4bd image tag 2026-04-27 13:00:37 +02:00
7405ce27dd chart name 2026-04-27 12:55:20 +02:00
1281e8ef37 databunker 2026-04-27 12:54:18 +02:00
c497c54e8e fix 2026-04-27 12:28:47 +02:00
b57459cf85 rm secrets2 2026-04-27 12:25:25 +02:00
e8dd213685 rm secrets 2026-04-27 12:24:14 +02:00
1d879c82f9 secrets shuffle 2026-04-27 12:21:50 +02:00
94c8265475 overlays2 2026-04-27 12:01:59 +02:00
17d7c4a655 overlays 2026-04-27 11:49:10 +02:00
f3dba72c5d aks-dev 2026-04-27 11:33:24 +02:00
cc9c9049eb ignore diff 2026-04-26 23:55:55 +02:00
9f6c5105af netpol all remove 2026-04-25 16:04:13 +02:00
45e502d74d argocd tls 2026-04-25 11:49:17 +02:00
167d893233 clean scopes gitea 2026-04-24 20:18:02 +02:00
8b9ffee242 socpes 2026-04-24 20:14:28 +02:00
4069e255a8 org scope 2026-04-24 20:05:01 +02:00
3b1f498616 Update infra/values/base/keycloak-values.yaml 2026-04-24 17:40:15 +00:00
cc47bf6b9f grafana access 2026-04-24 15:49:47 +02:00
c1d61398f0 SSO grafana 2026-04-24 15:45:50 +02:00
ece4a8d199 grafana tls 2026-04-24 15:39:46 +02:00
03c47ad109 remove trivy 2026-04-24 15:24:58 +02:00
3095741590 clear KC scopes 2026-04-24 15:13:58 +02:00
d7ba859e61 no openid 2026-04-24 15:09:10 +02:00
07eb9b7051 optional scopes 2026-04-24 15:05:07 +02:00
a911ff64c3 kc scopes 2026-04-24 15:03:14 +02:00
9e13560e5e basic scope 2026-04-24 14:40:32 +02:00
3d84acb278 DEFAULT_EMAIL_NOTIFICATIONS 2026-04-24 14:25:29 +02:00
fde81c6ec6 dbox 2026-04-24 13:42:52 +02:00
8648269e55 Update secrets/base/kustomization.yaml 2026-04-24 11:25:38 +00:00
84fe4cbe7c ts-mcp-secrets-sealed.yaml 2026-04-24 13:15:00 +02:00
38158be0a8 doc 2026-04-24 12:55:50 +02:00
202e84badc doc 2026-04-24 12:54:26 +02:00
a6df75de93 dbox 2026-04-24 12:38:50 +02:00
4f4f544100 k 2026-04-24 10:58:20 +02:00
8d4b6493a0 mm 2026-04-24 10:57:53 +02:00
8505481291 feature/multi-cloud (#14)
Co-authored-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
Reviewed-on: #14
2026-04-24 08:48:53 +00:00
65598c9297 karpor diffs 2026-04-24 09:47:52 +02:00
3f0f70699b karpor 2026-04-24 09:43:16 +02:00
06522b2f19 ts-mcp 2026-04-23 14:44:33 +02:00
4c65035485 ns 2026-04-23 14:11:45 +02:00
84f4bebc08 ts-mcp 2026-04-23 13:41:51 +02:00
5394b2c714 ts-mcp 2026-04-23 13:40:33 +02:00
c4e586a7be ts-mcp 2026-04-23 13:38:47 +02:00
1fa070b041 argo 2026-04-23 13:35:42 +02:00
9c905355e3 argocd known host 2026-04-23 13:28:34 +02:00
6b1115ec28 argocd disable submodule 2026-04-23 13:09:02 +02:00
2fb276a62c ts-mcp 2026-04-23 13:02:00 +02:00
3efe1b68ef auth doc 2026-04-23 10:05:15 +02:00
5df104beec sp 2026-04-22 13:54:51 +02:00
0ecfee3cf8 prompts 2026-04-22 13:51:38 +02:00
c88938adb5 feature/ai-review (#7)
Co-authored-by: gitea_admin <admin@forteapps.net>
Reviewed-on: #7
Co-authored-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
Co-committed-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
2026-04-22 09:30:02 +00:00
d05a16840e pr trigger 2026-04-22 09:11:40 +02:00
d7c7242aa1 submodule 2026-04-22 09:10:38 +02:00
3bf9fa7837 pr label 2026-04-22 08:48:05 +02:00
d2596568f2 version tag 2026-04-21 15:17:52 +02:00
2a3539350b AI-review (#6)
Co-authored-by: gitea_admin <admin@forteapps.net>
Reviewed-on: #6
Co-authored-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
Co-committed-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
2026-04-21 08:20:41 +00:00
f97b613c12 remove unneeded yml 2026-04-20 22:46:44 +02:00
9c7db11470 remove unneeded yml 2026-04-20 22:45:53 +02:00
219 changed files with 4415 additions and 2591 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Force LF line endings for shell scripts
*.sh text eol=lf

View File

@@ -0,0 +1,47 @@
name: AI Code Review
on:
pull_request:
types: [ labeled, synchronize ]
jobs:
ai-review:
if: >-
(github.event.action == 'synchronized' && contains(toJSON(github.event.pull_request.labels), 'ai-review')) || contains(toJSON(gitea.event.changes.added_labels), 'ai-review')
runs-on: ubuntu-latest
env:
AI_REVIEW_CONFIG_FILE_YAML: ./shared-prompts/iac/.ai-review.yaml
# VCS configuration
VCS__PROVIDER: GITEA
VCS__PIPELINE__OWNER: ${{ github.repository_owner }}
VCS__PIPELINE__REPO: ${{ github.event.repository.name }}
VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number }}
VCS__HTTP_CLIENT__API_URL: https://git.forteapps.net/api/v1
VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }}
# Review — disable fallback to see real Gitea API errors
REVIEW__INLINE_COMMENT_FALLBACK: "false"
# LLM configuration
LLM__PROVIDER: CLAUDE
LLM__META__MODEL: claude-sonnet-4-20250514
LLM__META__MAX_TOKENS: "4096"
LLM__HTTP_CLIENT__API_URL: https://api.anthropic.com
LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
token: ${{ secrets.AI_REVIEW_TOKEN }}
- name: Run inline review
uses: docker://nikitafilonov/ai-review:v0.64.0
with:
args: ai-review run-inline
- name: Run summary review
uses: docker://nikitafilonov/ai-review:v0.64.0
with:
args: ai-review run-summary

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "shared-prompts"]
path = shared-prompts
url = https://git.forteapps.net/Forte/ai-review-prompts.git

View File

@@ -1,7 +0,0 @@
standards_version: "2025.1"
last_configured: "2026-04-04"
components:
github-pages: "2025.1"
github-pages-generator: "mkdocs"
github-pages-source: "docs/"
github-pages-theme: "material"

117
README.md
View File

@@ -1,9 +1,9 @@
# Kubernetes Cluster - GitOps Configuration # Kubernetes Cluster - GitOps Configuration
> **Kubernetes cluster bootstrapping and GitOps configuration repository** using ArgoCD for UpCloud Managed Kubernetes > **Kubernetes cluster bootstrapping and GitOps configuration repository** using ArgoCD for multi-cloud Kubernetes (UpCloud, AWS EKS, Azure AKS, GCP GKE)
[![GitOps](https://img.shields.io/badge/GitOps-ArgoCD-blue)](https://argoproj.github.io/cd/) [![GitOps](https://img.shields.io/badge/GitOps-ArgoCD-blue)](https://argoproj.github.io/cd/)
[![Kubernetes](https://img.shields.io/badge/Kubernetes-UpCloud-orange)](https://upcloud.com/) [![Kubernetes](https://img.shields.io/badge/Kubernetes-Multi--Cloud-orange)]()
--- ---
@@ -57,11 +57,11 @@ This repository contains the complete GitOps configuration for our Kubernetes cl
### What's Inside ### What's Inside
- **Infrastructure Applications**: Traefik, Cert-Manager, Kyverno, Prometheus, Grafana, Loki, Tempo, Sealed Secrets - **Infrastructure Applications**: Traefik, Cert-Manager, Kyverno, Prometheus, Grafana, Loki, Tempo, Vault, Vault Secrets Operator, Homepage (platform dashboard)
- **Business Applications**: MCP10X, MusicMan, Dot-AI Stack, ArgoCD MCP - **Business Applications**: MCP10X, MusicMan, Dot-AI Stack, ArgoCD MCP
- **Policies**: Kyverno security policies for secret management, namespace controls, pod verification - **Policies**: Kyverno security policies for secret management, namespace controls, pod verification
- **Monitoring**: Full observability stack with metrics, logs, traces, and alerting - **Monitoring**: Full observability stack with metrics, logs, traces, and alerting
- **Secrets**: Sealed Secrets for secure Git storage - **Secrets**: Vault Secrets Operator (VSO) syncs secrets from HashiCorp Vault to K8s
### Key Features ### Key Features
@@ -84,31 +84,51 @@ This repository contains the complete GitOps configuration for our Kubernetes cl
├── _app-of-apps.yaml # Root ArgoCD Application (App-of-Apps pattern) ├── _app-of-apps.yaml # Root ArgoCD Application (App-of-Apps pattern)
├── infra/ # Infrastructure ArgoCD Applications (Kustomize multi-cluster) ├── infra/ # Infrastructure ArgoCD Applications (Kustomize multi-cluster)
│ ├── base/ # Base ArgoCD Application manifests (EU defaults) │ ├── base/ # Base ArgoCD Application manifests (one dir per component)
│ │ ├── kustomization.yaml │ │ ├── kustomization.yaml # Aggregates all component subdirectories
│ │ ├── traefik-application.yaml │ │ ├── traefik-application/
│ │ ├── keycloak.yaml │ │ ├── kustomization.yaml
│ │ ── grafana.yaml │ │ │ └── traefik-application.yaml
│ │ ├── gitea.yaml │ │ ├── keycloak/
│ │ ├── gitea-actions.yaml │ │ │ ├── kustomization.yaml
│ │ ├── tempo.yaml │ │ │ └── keycloak.yaml
│ │ ├── renovate.yaml │ │ ├── grafana/
│ │ ├── ... # All other Application manifests │ │ ├── prometheus/
│ │ ── secrets.yaml │ │ ── ... # Each component in its own subdirectory
├── overlays/ # Per-cluster overrides │ └── secrets/
│ ├── upc-dev/ # UpCloud Dev cluster (uses base as-is) │ ├── overlays/ # Per-cluster overrides (Kustomize)
│ │ ── upc-prod/ # UpCloud Prod cluster (patches value paths) │ │ ── upc-dev/ # UpCloud Dev — includes all base components
│ │ ├── upc-prod/ # UpCloud Prod — all components + patches
│ │ ├── aks-dev/ # Azure AKS Dev — selective components only
│ │ ├── aks-prod/ # Azure AKS Prod
│ │ ├── eks-dev/ # AWS EKS Dev
│ │ ├── eks-prod/ # AWS EKS Prod
│ │ ├── gke-dev/ # GCP GKE Dev
│ │ └── gke-prod/ # GCP GKE Prod
│ ├── dashboards/ # Grafana dashboard ConfigMaps │ ├── dashboards/ # Grafana dashboard ConfigMaps
│ └── values/ # Helm value overrides │ └── values/ # Helm value overrides
│ ├── base/ # Shared values (all clusters) │ ├── base/ # Shared cloud-agnostic values
│ ├── upc-dev/ # UpCloud Dev-specific values │ ├── upc-dev/ # UpCloud Dev (storage, LB, pricing)
── upc-prod/ # UpCloud Prod-specific values ── upc-prod/ # UpCloud Prod
│ ├── eks-dev/ # AWS EKS Dev
│ ├── eks-prod/ # AWS EKS Prod
│ ├── aks-dev/ # Azure AKS Dev
│ ├── aks-prod/ # Azure AKS Prod
│ ├── gke-dev/ # GCP GKE Dev
│ └── gke-prod/ # GCP GKE Prod
├── apps/ # Business Applications ├── apps/ # Business Applications (Kustomize, same pattern as infra)
│ ├── mcp10x.yaml │ ├── base/ # One subdirectory per app
│ ├── musicman.yaml │ ├── kustomization.yaml
│ ├── dot-ai-stack.yaml │ ├── musicman/
└── argo-mcp.yaml │ ├── mcp10x/
│ │ ├── dot-ai-stack/
│ │ ├── ts-mcp/
│ │ └── argo-mcp/
│ └── overlays/ # Per-cluster: cherry-pick or include all
│ ├── upc-dev/ # All apps
│ ├── upc-prod/ # All apps + patches
│ └── aks-dev/ # Selective apps only
├── cluster-resources/ # Cluster-wide Kubernetes resources ├── cluster-resources/ # Cluster-wide Kubernetes resources
│ ├── letsencrypt-issuer.yaml │ ├── letsencrypt-issuer.yaml
@@ -167,7 +187,7 @@ Developer commits code → CI/CD builds image → Updates helm-prod-values → A
**Quick version**: **Quick version**:
1. Create `apps/myapp.yaml` (ArgoCD Application manifest) 1. Create `apps/myapp.yaml` (ArgoCD Application manifest)
2. Create `helm-prod-values/myapp/values.yaml` (configuration) 2. Create `helm-prod-values/myapp/values.yaml` (configuration)
3. Create sealed secrets if needed 3. Write secrets to Vault and create VaultStaticSecret CRD if needed
4. Commit and push - ArgoCD auto-syncs! 4. Commit and push - ArgoCD auto-syncs!
### Update an Existing Application ### Update an Existing Application
@@ -180,22 +200,18 @@ Developer commits code → CI/CD builds image → Updates helm-prod-values → A
### Manage Secrets ### Manage Secrets
**See detailed guide**: [Developer Guide - Working with Secrets](docs/DEVELOPER-GUIDE.md#working-with-secrets) **See detailed guide**: [Vault Secrets Operator Reference](docs/vault-secrets-operator.md)
```bash ```bash
# Create plain secret # 1. Write secret to Vault
kubectl create secret generic myapp-creds \ vault kv put kv/myapp/myapp-creds KEY=value
--from-literal=KEY=value \
--dry-run=client -o yaml > private/myapp-creds.yaml
# Seal it # 2. Create VaultStaticSecret CRD (one-time, commit to git)
kubeseal --format=yaml --cert=pub-cert.pem \ # See docs/vault-secrets-operator.md for CRD template
< private/myapp-creds.yaml > secrets/myapp-creds-sealed.yaml
# Commit sealed version # 3. Rotate secrets — no git commit needed!
git add secrets/myapp-creds-sealed.yaml vault kv put kv/myapp/myapp-creds KEY=new-value
git commit -m "Add myapp credentials" # VSO picks up changes within 30 seconds
git push
``` ```
### Enable Authentication ### Enable Authentication
@@ -308,7 +324,7 @@ kubectl patch application myapp -n argocd \
## 🔐 Security ## 🔐 Security
### Secret Management ### Secret Management
-Sealed Secrets for Git storage -Vault Secrets Operator (VSO) for secret management
- ✅ Kyverno auto-clones secrets to namespaces - ✅ Kyverno auto-clones secrets to namespaces
- ❌ Never commit plain secrets - ❌ Never commit plain secrets
@@ -335,7 +351,8 @@ kubectl patch application myapp -n argocd \
| **Traefik** | Ingress controller | `traefik` | 2 | | **Traefik** | Ingress controller | `traefik` | 2 |
| **Cert-Manager** | TLS certificates | `cert-manager` | 1 | | **Cert-Manager** | TLS certificates | `cert-manager` | 1 |
| **Kyverno** | Policy engine | `kyverno` | 1 | | **Kyverno** | Policy engine | `kyverno` | 1 |
| **Sealed Secrets** | Secret encryption | `kube-system` | 1 | | **Vault** | Secret storage | `vault` | 1 |
| **Vault Secrets Operator** | Secret sync (Vault → K8s) | `vault-secrets-operator-system` | 1 |
| **Prometheus** | Metrics | `monitoring` | 1 | | **Prometheus** | Metrics | `monitoring` | 1 |
| **Grafana** | Dashboards | `monitoring` | 1 | | **Grafana** | Dashboards | `monitoring` | 1 |
| **Loki** | Logs | `monitoring` | 1 | | **Loki** | Logs | `monitoring` | 1 |
@@ -343,7 +360,6 @@ kubectl patch application myapp -n argocd \
| **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet | | **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet |
| **OpenCost** | Cost monitoring | `monitoring` | 1 | | **OpenCost** | Cost monitoring | `monitoring` | 1 |
| **Renovate** | Dependency updates | `renovate` | CronJob | | **Renovate** | Dependency updates | `renovate` | CronJob |
| **Trivy** | Vulnerability scanning | `trivy-system` | 1 |
**Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components) **Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components)
@@ -361,7 +377,7 @@ kubectl patch application myapp -n argocd \
## 📖 Key Concepts ## 📖 Key Concepts
### App-of-Apps Pattern ### App-of-Apps Pattern
`_app-of-apps.yaml` is the root Application that manages all other Applications in `infra/`. Kustomize overlays in `infra/overlays/{upc-dev,upc-prod}/` render the base Applications with per-cluster patches (e.g., swapping value file paths from `upc-dev` to `upc-prod`). `_app-of-apps-{cluster}.yaml` is the root Application that manages all other Applications in `infra/`. Each component in `infra/base/` lives in its own subdirectory (e.g., `infra/base/grafana/`). Overlays can either include **all** components (via `../../base`) or **cherry-pick** specific ones (via `../../base/grafana`, `../../base/prometheus`, etc.). Per-cluster patches swap Helm value file paths. Supported clusters: `upc-dev`, `upc-prod`, `eks-dev`, `eks-prod`, `aks-dev`, `aks-prod`, `gke-dev`, `gke-prod`.
### Multi-Source Pattern ### Multi-Source Pattern
Applications reference both: Applications reference both:
@@ -436,7 +452,7 @@ Applications deploy in order using `argocd.argoproj.io/sync-wave`:
1. Read [Developer Guide - Deploying Your First Application](docs/DEVELOPER-GUIDE.md#deploying-your-first-application) 1. Read [Developer Guide - Deploying Your First Application](docs/DEVELOPER-GUIDE.md#deploying-your-first-application)
2. Create ArgoCD Application manifest in `apps/` 2. Create ArgoCD Application manifest in `apps/`
3. Create Helm values in `helm-prod-values/` 3. Create Helm values in `helm-prod-values/`
4. Create sealed secrets if needed 4. Write secrets to Vault and create VaultStaticSecret CRD if needed
5. Commit and push - ArgoCD handles the rest! 5. Commit and push - ArgoCD handles the rest!
### Modifying Infrastructure ### Modifying Infrastructure
@@ -458,16 +474,14 @@ Documentation lives in `docs/`. To update:
## 📝 Notes ## 📝 Notes
### Current Environment ### Current Environment
- **Provider**: UpCloud Managed Kubernetes - **Provider**: Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE)
- **Active clusters**: UpCloud (upc-dev, upc-prod)
- **Environment**: Production (internal use only) - **Environment**: Production (internal use only)
- **Clusters**: Multi-cluster (upc-dev, upc-prod) via Kustomize overlays
- **Auth**: Disabled for ArgoCD (internal access) - **Auth**: Disabled for ArgoCD (internal access)
- **Backup**: None (cluster rebuildable via GitOps) - **Backup**: Gitea daily backup to S3-compatible storage
### Known Limitations ### Known Limitations
- No automated backups (yet)
- Secret rotation not automated - Secret rotation not automated
- Multi-cluster limited to upc-dev and upc-prod environments
- DNS management is manual - DNS management is manual
**Future improvements**: See [Operations Runbook - Disaster Recovery](docs/OPERATIONS-RUNBOOK.md#disaster-recovery) **Future improvements**: See [Operations Runbook - Disaster Recovery](docs/OPERATIONS-RUNBOOK.md#disaster-recovery)
@@ -482,7 +496,8 @@ Documentation lives in `docs/`. To update:
- [Traefik Documentation](https://doc.traefik.io/traefik/) - [Traefik Documentation](https://doc.traefik.io/traefik/)
- [Cert-Manager Documentation](https://cert-manager.io/docs/) - [Cert-Manager Documentation](https://cert-manager.io/docs/)
- [Grafana Tempo Documentation](https://grafana.com/docs/tempo/) - [Grafana Tempo Documentation](https://grafana.com/docs/tempo/)
- [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) - [Vault Secrets Operator](https://developer.hashicorp.com/vault/docs/platform/k8s/vso)
- [HashiCorp Vault](https://developer.hashicorp.com/vault/docs)
### Related Repositories ### Related Repositories
- [forte-helm](https://git.forteapps.net/Forte/forte-helm) - Helm chart templates - [forte-helm](https://git.forteapps.net/Forte/forte-helm) - Helm chart templates
@@ -504,7 +519,7 @@ Internal use only. Not for public distribution.
--- ---
**Last Updated**: 2026-03-16 **Last Updated**: 2026-04-22
**Documentation Version**: 1.0.0 **Documentation Version**: 1.0.0
**🚀 Ready to get started? Check out the [Documentation Index](docs/README.md)!** **🚀 Ready to get started? Check out the [Documentation Index](docs/README.md)!**

View File

@@ -1,33 +1,32 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
metadata: metadata:
name: network-policies name: infrastructure-apps
namespace: argocd namespace: argocd
labels: labels:
app.kubernetes.io/name: network-policies app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
finalizers: finalizers:
- resources-finalizer.argocd.argoproj.io - resources-finalizer.argocd.argoproj.io
spec: spec:
project: default project: default
source: source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD targetRevision: HEAD
path: cluster-resources/network path: infra/overlays/aks-dev
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: default
syncPolicy: syncPolicy:
automated: automated:
prune: true prune: true
selfHeal: true selfHeal: true
allowEmpty: false
syncOptions: syncOptions:
- Validate=true - CreateNamespace=true
- ServerSideApply=true

View File

@@ -0,0 +1,32 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/aks-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

32
_app-of-apps-eks-dev.yaml Normal file
View File

@@ -0,0 +1,32 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/eks-dev
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -0,0 +1,32 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/eks-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

32
_app-of-apps-gke-dev.yaml Normal file
View File

@@ -0,0 +1,32 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/gke-dev
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -0,0 +1,32 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/gke-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -18,7 +18,7 @@ metadata:
spec: spec:
project: default project: default
source: source:
repoURL: git@github.com:fortedigital/sturdy-adventure.git repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD targetRevision: HEAD
path: infra/overlays/upc-prod path: infra/overlays/upc-prod
destination: destination:

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: argocd-mcp-credentials
namespace: argocd-mcp
spec:
type: kv-v2
mount: kv
path: argocd-mcp/argocd-mcp-credentials
destination:
name: argocd-mcp-credentials
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: auth-oidc
namespace: argocd-mcp
spec:
type: kv-v2
mount: kv
path: argocd-mcp/auth-oidc
destination:
name: auth-oidc
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- argo-mcp.yaml
- vault-auth.yaml
- auth-oidc-vault.yaml
- argocd-mcp-credentials-vault.yaml
# Removed: argocdmcp-auth-oidc-sealed.yaml, argocd-mcp-credentials.yaml (migrated to VSO)

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-argocd-mcp
namespace: argocd-mcp
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: argocd-mcp
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-argocd-mcp
serviceAccount: vault-auth-argocd-mcp
audiences:
- vault

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: dot-ai-secrets
namespace: dot-ai
spec:
type: kv-v2
mount: kv
path: dot-ai/dot-ai-secrets
destination:
name: dot-ai-secrets
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -37,7 +37,7 @@ spec:
- $values/infra/values/base/dot-ai-stack-values.yaml - $values/infra/values/base/dot-ai-stack-values.yaml
- $values/infra/values/upc-dev/dot-ai-stack-values.yaml - $values/infra/values/upc-dev/dot-ai-stack-values.yaml
- repoURL: git@github.com:fortedigital/sturdy-adventure.git - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD targetRevision: HEAD
ref: values ref: values

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- dot-ai-stack.yaml
- vault-auth.yaml
- dot-ai-secrets-vault.yaml
# Removed: dot-ai-secrets.yaml (migrated to VSO)

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-dot-ai
namespace: dot-ai
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: dot-ai
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-dot-ai
serviceAccount: vault-auth-dot-ai
audiences:
- vault

View File

@@ -1,7 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- dot-ai-stack.yaml - dot-ai-stack
- mcp10x.yaml - mcp10x
- musicman.yaml - musicman
- argo-mcp.yaml - ts-mcp
- argo-mcp

View File

@@ -0,0 +1,15 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: app-credentials
namespace: mcp10x
spec:
type: kv-v2
mount: kv
path: mcp10x/app-credentials
destination:
name: app-credentials
create: true
type: Opaque
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- mcp10x.yaml
- vault-auth.yaml
- app-credentials-vault.yaml
# Removed: forte10x-app-credentials-sealed.yaml (migrated to VSO)

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-mcp10x
namespace: mcp10x
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: mcp10x
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-mcp10x
serviceAccount: vault-auth-mcp10x
audiences:
- vault

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- musicman.yaml
- vault-auth.yaml
- musicman-credentials-vault.yaml
# Removed: musicman-credentials.yaml (migrated to VSO)

View File

@@ -0,0 +1,15 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: musicman-credentials
namespace: music-man
spec:
type: kv-v2
mount: kv
path: music-man/musicman-credentials
destination:
name: musicman-credentials
create: true
type: Opaque
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -4,6 +4,8 @@ metadata:
creationTimestamp: null creationTimestamp: null
name: musicman-credentials name: musicman-credentials
namespace: music-man namespace: music-man
annotations:
argocd.argoproj.io/sync-wave: "12"
spec: spec:
encryptedData: encryptedData:
DATABASE_URL: AgBGLu8Rw9z9WMo3uX7fezN7tOVlEsmWtikFlyBxuSuQ1dCv6KTCePkwxJx4LuKvaHXlwdWl5yP8wQxMJP0BNJ1wewFb9zeUkP1YuCz4MrfuXq1zrecIr86R5hNbPiOb66e/4oOTCY/z3QREX9WjZdLJV/PCyBz8MP0D51pgWXpM6CBdhwpFbHSALyJk89+q44c9KkRxAUG2OLnesMeRe9nXJt5ariUCl9Qd2POIjx2hSNII1l0KbTcjI9hCf91DYM6poqKYYQUpnrjKv3LJwWS79I2b56+iTtroH3usIRgaiwgtFt2INm+8gwLBmC4xxKJ5VAjjYB/3dcN9XeboXvj0NB05P9jS3e77imUFANIB9coeaNlcvRWxwGCewYMp8+7RT7jPVA41/+aT/zT74tq9WhkKvgrr1It9/5fRnXtFEkhZg5bBcYCChzooarHkiwKlA3Wo0CrFsDPqy89oZrnwMRnVqKWBf79koZV4l7uCA0do9ojf55lTy8mt3mKQkwfqK9UdzZNbYzH0/Fk6gxlSxANOOqe7kt6VPywYUBnh6JS5U+kdTgNeSrFy/xqLFz28fXuikSJvLEouSFu66MeT+6uvYEmdfdLeh7quW/n+p7QTok3v3kRYJ/1Dl8ZtgvM7e8F/J5bLcacj394AJ/bBt+RIDa+XBjNNPrWKcWt/mkudZ25F/84G+hNxYQv7PIbhYfA1JTuHmQSoF+xah5QhKpyNpI3+knJmJj/4MhPKLnTuebg0xfbPevm2CU9fSa4sPIqmSvSGtqlXODvCfDSFEYzWfyfXV5Tys1NGAt04V8fl9A9UxULUm510NCeD0jzFeeYm3ZJiyavA5xF6hXCHoqLE DATABASE_URL: AgBGLu8Rw9z9WMo3uX7fezN7tOVlEsmWtikFlyBxuSuQ1dCv6KTCePkwxJx4LuKvaHXlwdWl5yP8wQxMJP0BNJ1wewFb9zeUkP1YuCz4MrfuXq1zrecIr86R5hNbPiOb66e/4oOTCY/z3QREX9WjZdLJV/PCyBz8MP0D51pgWXpM6CBdhwpFbHSALyJk89+q44c9KkRxAUG2OLnesMeRe9nXJt5ariUCl9Qd2POIjx2hSNII1l0KbTcjI9hCf91DYM6poqKYYQUpnrjKv3LJwWS79I2b56+iTtroH3usIRgaiwgtFt2INm+8gwLBmC4xxKJ5VAjjYB/3dcN9XeboXvj0NB05P9jS3e77imUFANIB9coeaNlcvRWxwGCewYMp8+7RT7jPVA41/+aT/zT74tq9WhkKvgrr1It9/5fRnXtFEkhZg5bBcYCChzooarHkiwKlA3Wo0CrFsDPqy89oZrnwMRnVqKWBf79koZV4l7uCA0do9ojf55lTy8mt3mKQkwfqK9UdzZNbYzH0/Fk6gxlSxANOOqe7kt6VPywYUBnh6JS5U+kdTgNeSrFy/xqLFz28fXuikSJvLEouSFu66MeT+6uvYEmdfdLeh7quW/n+p7QTok3v3kRYJ/1Dl8ZtgvM7e8F/J5bLcacj394AJ/bBt+RIDa+XBjNNPrWKcWt/mkudZ25F/84G+hNxYQv7PIbhYfA1JTuHmQSoF+xah5QhKpyNpI3+knJmJj/4MhPKLnTuebg0xfbPevm2CU9fSa4sPIqmSvSGtqlXODvCfDSFEYzWfyfXV5Tys1NGAt04V8fl9A9UxULUm510NCeD0jzFeeYm3ZJiyavA5xF6hXCHoqLE

View File

@@ -36,13 +36,8 @@ spec:
automated: automated:
prune: true prune: true
selfHeal: true selfHeal: true
allowEmpty: false
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- Validate=true
- ServerSideApply=false
- Replace=false
retry: retry:
limit: 5 limit: 5
backoff: backoff:

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-music-man
namespace: music-man
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: music-man
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-music-man
serviceAccount: vault-auth-music-man
audiences:
- vault

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ts-mcp.yaml
- vault-auth.yaml
- ts-mcp-secrets-vault.yaml
# Removed: ts-mcp-secrets-sealed.yaml (migrated to VSO)

View File

@@ -0,0 +1,13 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: ts-mcp-secrets
namespace: ts-mcp
spec:
encryptedData:
AZURE_CLIENT_SECRET: AgCWj525+NHkZ8XG97hEe4RS0SDC0QIGDXmEvzSlIqJQ9XVZEeKxVuAYmJ+w/HH7zBXD3qlZISeOPKn3FbMEeRukmYK0d5PsH26tRUMPoMzwWCuQkZIQ83uX9Pz/wMiqW8aZFIxpdEiUgVdanxHSFoDRPC1VlSEtV9B9yN2MgXBID5s0oje5BM9ttc4WVRe6+9pMeaOC6u+YUgcfY7xPLetZfC9nQO4zn4jYhoQXfAddwMzNODvQNGPzIv6PXDXJweTwdmtGaxM6eDdcCJI/30bEV9prA5m6UlgTZ/Qp+onU70KdkBA9gM9tMMVUR6j/2sbWzqMP/rVaFLeUH1PjHv15n4EieWyuDyYEfmZNDFXc7O9RIK6P0jCIE+t3myxK2ZQ7cfXprdOSj94au0qP6leat0UUVoc9CFJHHtrNxXYWl7IYVhwvIQCMSgO2qoAXkdW4wKVJAcbJadJjoL2pWxzjaD4GgnUaAxWBANqZI2lD8CED4VfUVMB0ZUYRS/zvy/eqIGlT8WbzwTYFi3YDZRvAUIknxaWEavIG4x52d0FqTmFYY06W53fGYfBrUjJI54GWYyBpKdZTf7b/AlAN0+kwkk6OqsUWwWDqxR7LVCcPhjSIKd/THp+Tbq9z5TiPIHxOO9V60u51f8IoQrEgQfNov7CEGQZ8B9HUGObjNc5MhujzBJasMhrUcd2Ddk6KWk07B7223p/gIEM+81ZWQYUcc29+U/j1dQyRNZy/TC56ywe5DDBJSoGp
template:
metadata:
name: ts-mcp-secrets
namespace: ts-mcp

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: ts-mcp-secrets
namespace: ts-mcp
spec:
type: kv-v2
mount: kv
path: ts-mcp/ts-mcp-secrets
destination:
name: ts-mcp-secrets
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -1,27 +1,37 @@
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
metadata: metadata:
name: secrets name: ts-mcp
namespace: argocd namespace: argocd
annotations: annotations:
argocd.argoproj.io/sync-wave: "2" argocd.argoproj.io/sync-wave: "11"
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: "" notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
notifications.argoproj.io/subscribe.on-sync-failed.slack: "" notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
notifications.argoproj.io/subscribe.on-degraded.slack: "" notifications.argoproj.io/subscribe.on-degraded.slack: ""
labels: labels:
app.kubernetes.io/name: secrets app.kubernetes.io/name: ts-mcp
app.kubernetes.io/part-of: platform app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd app.kubernetes.io/managed-by: argocd
finalizers: finalizers:
- resources-finalizer.argocd.argoproj.io - resources-finalizer.argocd.argoproj.io
spec: spec:
project: default project: default
source: sources:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git - repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
path: secrets/upc-dev path: forteapp
targetRevision: HEAD
helm:
valueFiles:
- $values/ts-mcp/values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
targetRevision: HEAD
ref: values
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: secrets namespace: ts-mcp
syncPolicy: syncPolicy:
automated: automated:
prune: true prune: true

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-ts-mcp
namespace: ts-mcp
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: ts-mcp
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-ts-mcp
serviceAccount: vault-auth-ts-mcp
audiences:
- vault

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base/musicman

View File

@@ -0,0 +1,47 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: dbunk-demo
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "12"
labels:
app.kubernetes.io/name: dbunk-demo
app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
path: forteapp
targetRevision: HEAD
helm:
valueFiles:
- $values/dbunk-demo/values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: dbunk-demo
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- dbunk-demo.yaml

View File

@@ -0,0 +1,53 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: feedback
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "12"
labels:
app.kubernetes.io/name: feedback
app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
path: forteapp
targetRevision: HEAD
helm:
valueFiles:
- $values/feedback/values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: feedback
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: StatefulSet
jsonPointers:
- /spec/volumeClaimTemplates

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- feedback.yaml

View File

@@ -2,6 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- ../../base - ../../base
- dbunk-demo
- feedback
# No patches needed — base already has "upc-dev" paths # No patches needed — base already has "upc-dev" paths
# upc-dev is the default/base cluster # upc-dev is the default/base cluster

View File

@@ -1,8 +1,9 @@
#!/bin/zsh #!/bin/zsh
# in case of $'\r': command not found error, run command below first # in case of $'\r': command not found error, run command below first
# sed -i 's/\r$//' ./bootstrap.sh # sed -i 's/\r$//' ./bootstrap.sh
CLUSTER="${1:?Usage: ./bootstrap.sh <cluster> (upc-dev|upc-prod)}" CLUSTER="${1:?Usage: ./bootstrap.sh <cluster> (upc-dev|upc-prod|aks-dev|aks-prod|eks-dev|eks-prod|gke-dev|gke-prod)}"
echo "running $0 for cluster: ${CLUSTER}..." echo "running $0 for cluster: ${CLUSTER}..."
@@ -17,7 +18,7 @@ echo "Bootstrapping cluster: ${clusterName} (${CLUSTER})..."
Bootstrap() Bootstrap()
{ {
ArgoCd ArgoCd
# Gitea Gitea
} }
@@ -27,8 +28,8 @@ Bootstrap()
Gitea() Gitea()
{ {
echo "Installing secret..." echo "Installing secret..."
kubectl apply -f private/gitea-repo-main.yaml kubectl apply -f "private/${CLUSTER}/gitea-repo-main.yaml"
kubectl apply -f private/main.key kubectl apply -f "private/${CLUSTER}/main.key"
} }
############################################################ ############################################################
@@ -36,10 +37,15 @@ Gitea()
############################################################ ############################################################
ArgoCd() ArgoCd()
{ {
# Pre-create ConfigMap for repo-server env (must exist before Helm upgrade)
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f cluster-resources/argocd-repo-server-config.yaml
# install argocd # install argocd
echo "Installing ArgoCD..." echo "Installing ArgoCD..."
helm upgrade --install argocd argo-cd \ helm upgrade --install argocd argo-cd \
--repo https://argoproj.github.io/argo-helm \ --repo https://argoproj.github.io/argo-helm \
--version "7.8.0" \
--namespace argocd --create-namespace \ --namespace argocd --create-namespace \
--values infra/values/base/argocd-values.yaml \ --values infra/values/base/argocd-values.yaml \
--values "infra/values/${CLUSTER}/argocd-values.yaml" \ --values "infra/values/${CLUSTER}/argocd-values.yaml" \
@@ -49,4 +55,4 @@ ArgoCd()
kubectl apply -f "_app-of-apps-${CLUSTER}.yaml" -n argocd kubectl apply -f "_app-of-apps-${CLUSTER}.yaml" -n argocd
} }
# Bootstrap Bootstrap

View File

@@ -0,0 +1,15 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: argocd-notifications-secret
namespace: argocd
spec:
type: kv-v2
mount: kv
path: argocd/argocd-notifications-secret
destination:
name: argocd-notifications-secret
create: true
type: Opaque
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,83 @@
# CronJob: syncs OIDC client secret from registrar-managed
# argocd-oidc-credentials into argocd-secret (oidc.clientSecret key).
# Runs every 2 min. No-ops if source secret doesn't exist yet
# (safe for fresh deploys before Keycloak is up).
apiVersion: v1
kind: ServiceAccount
metadata:
name: argocd-oidc-sync
namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: argocd-oidc-sync
namespace: argocd
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["argocd-oidc-credentials", "argocd-secret"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argocd-oidc-sync
namespace: argocd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: argocd-oidc-sync
subjects:
- kind: ServiceAccount
name: argocd-oidc-sync
namespace: argocd
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: argocd-oidc-sync
namespace: argocd
spec:
schedule: "*/2 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
serviceAccountName: argocd-oidc-sync
restartPolicy: Never
containers:
- name: sync
image: bitnami/kubectl:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
# Exit gracefully if source secret doesn't exist yet
if ! kubectl get secret argocd-oidc-credentials -n argocd >/dev/null 2>&1; then
echo "argocd-oidc-credentials not found — skipping (Keycloak not ready yet)"
exit 0
fi
# Read current OIDC client secret
NEW_SECRET=$(kubectl get secret argocd-oidc-credentials -n argocd \
-o jsonpath='{.data.client-secret}' | base64 -d)
# Read current value in argocd-secret (if any)
CURRENT=$(kubectl get secret argocd-secret -n argocd \
-o jsonpath='{.data.oidc\.clientSecret}' 2>/dev/null | base64 -d || echo "")
# Only patch if changed
if [ "$NEW_SECRET" = "$CURRENT" ]; then
echo "oidc.clientSecret already up to date"
exit 0
fi
kubectl patch secret argocd-secret -n argocd --type merge \
-p "{\"stringData\":{\"oidc.clientSecret\":\"${NEW_SECRET}\"}}"
echo "Patched argocd-secret with oidc.clientSecret"

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-repo-server-config
namespace: argocd
data:
# Disable git submodule checkout - submodules (e.g. shared-prompts)
# are not needed for K8s manifest generation
ARGOCD_GIT_MODULES_ENABLED: "false"

View File

@@ -0,0 +1,16 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: forte-helm-repo
namespace: argocd
spec:
type: kv-v2
mount: kv
path: argocd/forte-helm-repo
destination:
name: forte-helm-repo
create: true
labels:
argocd.argoproj.io/secret-type: repository
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,17 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: forte10x-repo-creds
namespace: argocd
spec:
type: kv-v2
mount: kv
path: argocd/forte10x-repo-creds
destination:
name: forte10x-repo-creds
create: true
type: Opaque
labels:
argocd.argoproj.io/secret-type: repository
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -57,17 +57,17 @@ spec:
- sh - sh
- -c - -c
- | - |
mc alias set upcloud "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}" mc alias set s3 "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S) TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip" KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..." echo "Uploading ${KEY}..."
mc cp /backup/gitea-dump.zip "upcloud/${S3_BUCKET}/${KEY}" && \ mc cp /backup/gitea-dump.zip "s3/${S3_BUCKET}/${KEY}" && \
echo "Upload complete." echo "Upload complete."
# Prune backups older than 7 days # Prune backups older than 7 days
echo "Pruning backups older than 7 days..." echo "Pruning backups older than 7 days..."
mc rm --older-than 7d --force "upcloud/${S3_BUCKET}/" 2>&1 || true mc rm --older-than 7d --force "s3/${S3_BUCKET}/" 2>&1 || true
echo "Pruning complete." echo "Pruning complete."
envFrom: envFrom:
- secretRef: - secretRef:

View File

@@ -0,0 +1,17 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: mcp10x-repo-creds
namespace: argocd
spec:
type: kv-v2
mount: kv
path: argocd/mcp10x-repo-creds
destination:
name: mcp10x-repo-creds
create: true
type: Opaque
labels:
argocd.argoproj.io/secret-type: repository
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -245,6 +245,12 @@ spec:
secretKeyRef: secretKeyRef:
name: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-credentials-secret\" || 'auth-oidc' }}" name: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-credentials-secret\" || 'auth-oidc' }}"
key: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-credentials-secret-key\" || 'client-secret' }}" key: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-credentials-secret-key\" || 'client-secret' }}"
- name: AUTH_OIDC_IDP_HINT
value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-idp-hint\" || '' }}"
- name: AUTH_OIDC_BROKER_ALIAS
value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-broker-alias\" || '' }}"
- name: AUTH_OIDC_BROKER_TOKEN_HEADER
value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-oidc-broker-token-header\" || '' }}"
resources: resources:
limits: limits:
cpu: 50m cpu: 50m
@@ -324,6 +330,8 @@ spec:
value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-public-paths\" || '/healthz' }}" value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-public-paths\" || '/healthz' }}"
- name: AUTH_MCP_SCOPES_SUPPORTED - name: AUTH_MCP_SCOPES_SUPPORTED
value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-scopes\" || 'profile' }}" value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-scopes\" || 'profile' }}"
- name: AUTH_MCP_IDP_HINT
value: "{{ request.object.metadata.annotations.\"policies.forteapps.io/auth-mcp-idp-hint\" || '' }}"
resources: resources:
limits: limits:
cpu: 50m cpu: 50m

View File

@@ -26,7 +26,6 @@ spec:
- monitoring - monitoring
- secrets - secrets
- kyverno - kyverno
- trivy-system
match: match:
any: any:
- resources: - resources:

View File

@@ -16,7 +16,6 @@ spec:
- resources: - resources:
namespaces: namespaces:
- kube-system - kube-system
- trivy-system
- monitoring - monitoring
- argocd - argocd
- cert-manager - cert-manager

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-argocd
namespace: argocd
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: argocd
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-argocd
serviceAccount: vault-auth-argocd
audiences:
- vault

12
clusters/aks-dev.yaml Normal file
View File

@@ -0,0 +1,12 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: k8s-launchpad # → infra/values/aks-dev/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/aks-dev/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/aks-dev/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/aks-dev/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/aks-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/aks-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # → infra/values/aks-dev/traefik-values.yaml (ports.*.trustedIPs) — VNet CIDR + Azure health probe
cloudProvider: azure # → determines overlay directory and cloud-specific LB/storage annotations

12
clusters/aks-prod.yaml Normal file
View File

@@ -0,0 +1,12 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: prod-aks # → infra/values/aks-prod/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/aks-prod/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/aks-prod/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/aks-prod/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/aks-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/aks-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # → infra/values/aks-prod/traefik-values.yaml (ports.*.trustedIPs) — VNet CIDR + Azure health probe
cloudProvider: azure # → determines overlay directory and cloud-specific LB/storage annotations

12
clusters/eks-dev.yaml Normal file
View File

@@ -0,0 +1,12 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: dev-eks # → infra/values/eks-dev/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/eks-dev/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/eks-dev/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/eks-dev/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/eks-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/eks-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8" # → infra/values/eks-dev/traefik-values.yaml (ports.*.trustedIPs) — VPC CIDR
cloudProvider: eks # → determines overlay directory and cloud-specific LB/storage annotations

12
clusters/eks-prod.yaml Normal file
View File

@@ -0,0 +1,12 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: prod-eks # → infra/values/eks-prod/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/eks-prod/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/eks-prod/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/eks-prod/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/eks-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/eks-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8" # → infra/values/eks-prod/traefik-values.yaml (ports.*.trustedIPs) — VPC CIDR
cloudProvider: eks # → determines overlay directory and cloud-specific LB/storage annotations

12
clusters/gke-dev.yaml Normal file
View File

@@ -0,0 +1,12 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: dev-gke # → infra/values/gke-dev/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/gke-dev/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/gke-dev/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/gke-dev/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/gke-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/gke-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # → infra/values/gke-dev/traefik-values.yaml (ports.*.trustedIPs) — subnet + GCP health checks
cloudProvider: gke # → determines overlay directory and cloud-specific LB/storage annotations

12
clusters/gke-prod.yaml Normal file
View File

@@ -0,0 +1,12 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: prod-gke # → infra/values/gke-prod/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/gke-prod/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/gke-prod/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/gke-prod/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/gke-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/gke-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # → infra/values/gke-prod/traefik-values.yaml (ports.*.trustedIPs) — subnet + GCP health checks
cloudProvider: gke # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,10 +1,12 @@
clusterName: dev-fd-no-svg1 # Cluster config reference — values must match the corresponding overlay files.
domain: forteapps.net # Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
argocdDomain: argocd.127.0.0.1.nip.io clusterName: dev-fd-no-svg1 # → infra/values/upc-dev/argocd-values.yaml (notifications.context.clusterName)
grafanaDomain: grafana.forteapps.net domain: forteapps.net # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
keycloakDomain: id.forteapps.net argocdDomain: argocd.127.0.0.1.nip.io # → infra/values/upc-dev/argocd-values.yaml (global.domain)
dotaiDomain: kubemcp.forteapps.net grafanaDomain: grafana.forteapps.net # → infra/values/upc-dev/grafana-values.yaml (ingress.hosts)
dotaiUiDomain: kubemcpui.forteapps.net keycloakDomain: id.forteapps.net # → infra/values/upc-dev/keycloak-values.yaml (ingress.hostname)
letsencryptEmail: danijels@gmail.com dotaiDomain: kubemcp.forteapps.net # → infra/values/upc-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host)
trustedIPs: "172.16.1.0/24" dotaiUiDomain: kubemcpui.forteapps.net # → infra/values/upc-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host)
cloudProvider: upcloud letsencryptEmail: danijels@gmail.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "172.16.1.0/24" # → infra/values/upc-dev/traefik-values.yaml (ports.*.trustedIPs)
cloudProvider: upcloud # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,10 +1,12 @@
clusterName: prod-fd-no-svg1 # Cluster config reference — values must match the corresponding overlay files.
domain: fortedigital.com # Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
argocdDomain: argocd.127.0.0.1.nip.io clusterName: prod-fd-no-svg1 # → infra/values/upc-prod/argocd-values.yaml (notifications.context.clusterName)
grafanaDomain: grafana.fortedigital.com domain: fortedigital.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
keycloakDomain: id.fortedigital.com argocdDomain: argocd.127.0.0.1.nip.io # → infra/values/upc-prod/argocd-values.yaml (global.domain)
dotaiDomain: kubemcp.fortedigital.com grafanaDomain: grafana.fortedigital.com # → infra/values/upc-prod/grafana-values.yaml (ingress.hosts)
dotaiUiDomain: kubemcpui.fortedigital.com keycloakDomain: id.fortedigital.com # → infra/values/upc-prod/keycloak-values.yaml (ingress.hostname)
letsencryptEmail: danijel.simeunovic@fortedigital.com dotaiDomain: kubemcp.fortedigital.com # → infra/values/upc-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host)
trustedIPs: "172.16.1.0/24" dotaiUiDomain: kubemcpui.fortedigital.com # → infra/values/upc-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host)
cloudProvider: upcloud letsencryptEmail: danijel.simeunovic@fortedigital.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "172.16.1.0/24" # → infra/values/upc-prod/traefik-values.yaml (ports.*.trustedIPs)
cloudProvider: upcloud # → determines overlay directory and cloud-specific LB/storage annotations

32
devbox.json Normal file
View File

@@ -0,0 +1,32 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": [
"kubectl@1.33.2",
"kubernetes-helm@3.18.4",
"k9s@0.50.7",
"kubeseal@0.30.0",
"argocd@2.14.11",
"kubecm@0.33.1",
"kubectl-tree@0.4.3",
"kind@0.29.0",
"kustomize@5.7.0",
"kyverno@1.14.3",
"syft@1.29.0",
"grype@0.92.2",
"traefik@3.6.7",
"claude-code@latest",
"go@latest",
"dotnet-sdk@latest",
"opentofu@1.11.6"
],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"test": [
"echo \"Error: no test specified\" && exit 1"
]
}
}
}

View File

@@ -60,18 +60,16 @@ If you do need cluster access, install:
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
``` ```
2. **kubeseal** - For sealing secrets 2. **vault** CLI - For managing secrets in HashiCorp Vault
```bash ```bash
# macOS # macOS
brew install kubeseal brew install hashicorp/tap/vault
# Windows # Windows
choco install kubeseal choco install vault
# Linux # Linux
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/kubeseal-0.24.0-linux-amd64.tar.gz # See https://developer.hashicorp.com/vault/install
tar -xvzf kubeseal-0.24.0-linux-amd64.tar.gz
sudo mv kubeseal /usr/local/bin/
``` ```
3. **Git** - Version control 3. **Git** - Version control
@@ -634,125 +632,100 @@ git push
### Understanding Secret Management ### Understanding Secret Management
**NEVER commit plain secrets to Git.** We use **Sealed Secrets** to encrypt secrets before committing. Secrets are managed via **HashiCorp Vault** and synced to Kubernetes by the **Vault Secrets Operator (VSO)**. See [Vault Secrets Operator Reference](vault-secrets-operator.md) for full details.
**NEVER commit plain secret values to Git.** Only VaultStaticSecret CRD manifests are committed.
### Creating a New Secret ### Creating a New Secret
#### Step 1: Create Plain Secret Locally #### Step 1: Write Secret to Vault
```bash ```bash
cd ~/dev/k8s/launchpad vault kv put kv/myapp/myapp-credentials \
API_KEY=your-secret-key-here \
# Create secret in private/ folder (Git-ignored) DB_PASSWORD=super-secret-password
kubectl create secret generic myapp-credentials \
--from-literal=API_KEY=your-secret-key-here \
--from-literal=DB_PASSWORD=super-secret-password \
--dry-run=client -o yaml > private/myapp-credentials.yaml
``` ```
**DO NOT commit this file!** It's in `private/` which is Git-ignored. #### Step 2: Create VaultStaticSecret CRD
#### Step 2: Seal the Secret Create a YAML file (e.g., `apps/base/myapp/myapp-credentials-vault.yaml`):
Get the public certificate (one-time setup): ```yaml
apiVersion: secrets.hashicorp.com/v1beta1
```bash kind: VaultStaticSecret
# Fetch public cert from cluster metadata:
kubeseal --fetch-cert \ name: myapp-credentials
--controller-name=sealed-secrets-controller \ namespace: myapp
--controller-namespace=kube-system \ spec:
> pub-cert.pem type: kv-v2
mount: kv
path: myapp/myapp-credentials
destination:
name: myapp-credentials
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth
``` ```
Seal your secret: #### Step 3: Add VaultAuth (if new namespace)
If this is a new namespace, also create a `vault-auth.yaml` with a ServiceAccount and VaultAuth CRD. See [VSO Reference](vault-secrets-operator.md#vaultauth) for template.
#### Step 4: Commit and Push
```bash ```bash
kubeseal --format=yaml \ git add apps/base/myapp/myapp-credentials-vault.yaml
--cert=pub-cert.pem \ git commit -m "Add myapp credentials (VSO)"
< private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml
```
#### Step 3: Commit Sealed Secret
```bash
git add secrets/myapp-credentials-sealed.yaml
git commit -m "Add myapp credentials (sealed)"
git push git push
``` ```
#### Step 4: Reference Secret in Application ArgoCD syncs the CRD, VSO creates the K8s Secret.
#### Step 5: Reference Secret in Application
Update your `helm-prod-values/myapp/values.yaml`: Update your `helm-prod-values/myapp/values.yaml`:
```yaml ```yaml
app: app:
envSecretName: "myapp-credentials" # References the SealedSecret envSecretName: "myapp-credentials" # VSO creates this K8s Secret
``` ```
Commit and push: ### Updating / Rotating a Secret
**No git commit needed** — just update in Vault:
```bash ```bash
cd ~/dev/k8s/helm-prod-values vault kv put kv/myapp/myapp-credentials \
git add myapp/values.yaml API_KEY=new-key-here \
git commit -m "Reference myapp credentials" DB_PASSWORD=new-password
git push
``` ```
### Updating a Secret VSO picks up changes within 30 seconds. Restart pods if they don't watch for secret updates:
To update an existing secret:
```bash ```bash
# 1. Create new version of secret
kubectl create secret generic myapp-credentials \
--from-literal=API_KEY=new-key-here \
--from-literal=DB_PASSWORD=new-password \
--dry-run=client -o yaml > private/myapp-credentials.yaml
# 2. Seal it
kubeseal --format=yaml \
--cert=pub-cert.pem \
< private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml
# 3. Commit sealed version
git add secrets/myapp-credentials-sealed.yaml
git commit -m "Update myapp credentials"
git push
# 4. Restart pods to pick up new secret
kubectl rollout restart deployment myapp -n myapp kubectl rollout restart deployment myapp -n myapp
``` ```
### Secret Best Practices ### Secret Best Practices
✅ **DO**: - Write secrets to Vault via UI or CLI — never commit values to Git
- Store secrets in `private/` folder locally - Use meaningful secret names matching the KV path convention: `kv/{namespace}/{secret-name}`
- Always seal secrets before committing
- Delete plain secrets after sealing
- Use meaningful secret names
- Document what each secret contains - Document what each secret contains
- Use Vault's versioning for audit trail
❌ **DON'T**:
- Commit plain secrets to Git
- Share secrets via Slack/email
- Hard-code secrets in code
- Use the same secret across multiple environments
- Store secrets in Docker images
### Where Secrets Are Stored ### Where Secrets Are Stored
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────
│ Location │ Content │ Committed? │ Location │ Content │ In Git?
├──────────────────────────────────────────────┼──────────── ├────────────────────────────┼─────────────────────────┼──────────┤
private/ │ Plain secrets │ ❌ NO Vault KV (kv/{ns}/{name}) │ Secret values │ ❌ NO │
secrets/ │ Sealed secrets │ ✅ YES VaultStaticSecret CRD Sync config (no values)│ ✅ YES │
│ Kubernetes cluster │ Unsealed secrets │ N/A │ Kubernetes cluster K8s Secret (synced) │ N/A │
└─────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────
``` ```
**Sealed Secrets Controller** in the cluster decrypts sealed secrets automatically. **Vault Secrets Operator** syncs secrets from Vault to K8s automatically (30s refresh).
--- ---
@@ -886,28 +859,13 @@ In your identity provider (e.g., Keycloak):
#### Step 2: Create OIDC Secret #### Step 2: Create OIDC Secret
```bash ```bash
# Create plain secret # Write OIDC secret to Vault
kubectl create secret generic auth-oidc \ vault kv put kv/myapp/auth-oidc \
--from-literal=client-secret=your-oidc-client-secret \ client-secret=your-oidc-client-secret \
--from-literal=cookie-secret=$(openssl rand -hex 32) \ cookie-secret=$(openssl rand -hex 32)
--namespace=myapp \
--dry-run=client -o yaml > private/myapp-auth-oidc.yaml
# Seal it # Create VaultStaticSecret CRD (see docs/vault-secrets-operator.md for template)
kubeseal --format=yaml \ # Add to apps/base/myapp/auth-oidc-vault.yaml and commit
--cert=pub-cert.pem \
--namespace=myapp \
< private/myapp-auth-oidc.yaml \
> secrets/myapp-auth-oidc-sealed.yaml
# Commit sealed secret
cd ~/dev/k8s/launchpad
git add secrets/myapp-auth-oidc-sealed.yaml
git commit -m "Add OIDC secrets for myapp"
git push
# Clean up
rm private/myapp-auth-oidc.yaml
``` ```
#### Step 3: Configure Helm Values #### Step 3: Configure Helm Values
@@ -962,6 +920,46 @@ User sees application (authenticated)
--- ---
### Accessing Authenticated User Information
The auth sidecar handles all authentication before requests reach your application. Your app never sees unauthenticated traffic — the sidecar returns 401 or redirects to the IdP first.
After successful authentication, the sidecar forwards the request to your application with user identity injected as HTTP headers:
| Header | Description | Available in |
|--------|-------------|-------------|
| `X-Auth-User` | Username or display name | Token, OIDC, MCP |
| `X-Auth-Email` | User email address | OIDC |
| `X-Auth-Subject` | OIDC `sub` claim (stable user ID) | OIDC, MCP |
| `X-Auth-Groups` | Comma-separated group memberships | OIDC (if scope includes `groups`) |
| `X-Auth-Token` | The validated access token | All modes |
**Your application reads these headers — no auth library needed:**
```javascript
// Express.js example
app.get('/profile', (req, res) => {
const user = req.headers['x-auth-user'];
const email = req.headers['x-auth-email'];
res.json({ user, email });
});
```
```python
# Flask example
@app.route('/profile')
def profile():
user = request.headers.get('X-Auth-User')
email = request.headers.get('X-Auth-Email')
return jsonify(user=user, email=email)
```
**Why this is safe**: The Kyverno-generated NetworkPolicy restricts ingress to the sidecar port only. Traffic cannot bypass the sidecar to reach the application port directly, so the `X-Auth-*` headers can be trusted unconditionally.
**Key principle**: Your application is zero-trust-unaware by design. It reads headers and renders UI. All authentication complexity lives in the sidecar and Kyverno policy.
---
### Authentication Configuration Reference ### Authentication Configuration Reference
#### Helm Values Schema #### Helm Values Schema
@@ -1097,16 +1095,13 @@ ingress:
host: web-app.forteapps.net host: web-app.forteapps.net
``` ```
**With sealed OIDC secret**: **With Vault OIDC secret**:
```bash ```bash
# Create and seal secret # Write OIDC secret to Vault
kubectl create secret generic auth-oidc \ vault kv put kv/web-app/auth-oidc \
--from-literal=client-secret=super-secret-value \ client-secret=super-secret-value \
--from-literal=cookie-secret=$(openssl rand -hex 32) \ cookie-secret=$(openssl rand -hex 32)
--namespace=web-app \ # Then create VaultStaticSecret CRD — see docs/vault-secrets-operator.md
--dry-run=client -o yaml | \
kubeseal --format=yaml --cert=pub-cert.pem --namespace=web-app \
> secrets/web-app-auth-oidc-sealed.yaml
``` ```
#### Example 3: MCP Server with OAuth 2.0 #### Example 3: MCP Server with OAuth 2.0
@@ -1234,7 +1229,7 @@ kubectl logs -n myapp <pod-name> -c authn
- Use token auth for service-to-service communication - Use token auth for service-to-service communication
- Rotate tokens and secrets regularly - Rotate tokens and secrets regularly
- Use strong random tokens (32+ bytes) - Use strong random tokens (32+ bytes)
- Store client secrets in SealedSecrets - Store client secrets in Vault
- Test authentication before deploying to production - Test authentication before deploying to production
- Document which tokens/users have access - Document which tokens/users have access
@@ -1538,22 +1533,22 @@ curl http://localhost:8080
#### Problem: Secret not found #### Problem: Secret not found
**Check if SealedSecret exists:** **Check VSO sync status:**
```bash ```bash
kubectl get sealedsecret -n myapp kubectl get vaultstaticsecret -n myapp
kubectl get secret -n myapp kubectl get secret -n myapp
``` ```
**Solutions:** **Solutions:**
```bash ```bash
# Check if secret is in Git # Check VaultAuth is authenticated
ls -l secrets/myapp-credentials-sealed.yaml kubectl get vaultauth -n myapp
# Re-apply sealed secret # Check VaultStaticSecret events
kubectl apply -f secrets/myapp-credentials-sealed.yaml kubectl describe vaultstaticsecret myapp-credentials -n myapp
# Check sealed-secrets-controller logs # Verify secret exists in Vault
kubectl logs -n kube-system deployment/sealed-secrets-controller vault kv get kv/myapp/myapp-credentials
``` ```
#### Problem: Secret exists but pods can't access it #### Problem: Secret exists but pods can't access it
@@ -1664,7 +1659,7 @@ If you're stuck:
### Secret Management ### Secret Management
✅ **DO**: ✅ **DO**:
- Use kubeseal for all secrets - Use Vault for all secrets (see docs/vault-secrets-operator.md)
- Store plain secrets in password manager - Store plain secrets in password manager
- Rotate secrets regularly - Rotate secrets regularly
- Use different secrets per environment - Use different secrets per environment
@@ -1716,16 +1711,9 @@ kubectl rollout restart deployment myapp -n myapp
# Port-forward to service # Port-forward to service
kubectl port-forward -n myapp service/myapp 8080:3000 kubectl port-forward -n myapp service/myapp 8080:3000
# Create secret # Write secret to Vault
kubectl create secret generic myapp-credentials \ vault kv put kv/myapp/myapp-credentials KEY=value
--from-literal=KEY=value \ # Create VaultStaticSecret CRD — see docs/vault-secrets-operator.md
--dry-run=client -o yaml > private/myapp-credentials.yaml
# Seal secret
kubeseal --format=yaml \
--cert=pub-cert.pem \
< private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml
``` ```
### Repository Locations ### Repository Locations

View File

@@ -12,11 +12,11 @@
## Overview ## Overview
This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where Git repositories serve as the single source of truth for both infrastructure and application deployments. The cluster is running on **UpCloud Managed Kubernetes** but is designed to be cloud-agnostic. This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where Git repositories serve as the single source of truth for both infrastructure and application deployments. The cluster setup is **cloud-agnostic**, with ready-to-use configurations for **UpCloud**, **AWS EKS**, **Azure AKS**, and **GCP GKE**.
### Key Characteristics ### Key Characteristics
- **Environment**: Production (internal use only) - **Environment**: Production (internal use only)
- **Cluster Type**: Multi-cluster (upc-dev, upc-prod) via Kustomize overlays - **Cluster Type**: Multi-cloud, multi-cluster via Kustomize overlays (UpCloud, AWS, Azure, GCP)
- **GitOps Tool**: ArgoCD - **GitOps Tool**: ArgoCD
- **Deployment Pattern**: App-of-Apps - **Deployment Pattern**: App-of-Apps
- **Secret Management**: Sealed Secrets (kubeseal) - **Secret Management**: Sealed Secrets (kubeseal)
@@ -63,7 +63,7 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ Kubernetes Clusters │ │ Kubernetes Clusters │
│ (UpCloud: upc-dev, upc-prod) │ (UpCloud, AWS, Azure, GCP)
│ │ │ │
│ ┌──────────────────────────┐ │ │ ┌──────────────────────────┐ │
│ │ ArgoCD │ │ │ │ ArgoCD │ │
@@ -120,46 +120,47 @@ launchpad/
├── _app-of-apps-upc-prod.yaml # Root ArgoCD Application (upc-prod cluster) ├── _app-of-apps-upc-prod.yaml # Root ArgoCD Application (upc-prod cluster)
├── infra/ # Infrastructure ArgoCD Applications (Kustomize) ├── infra/ # Infrastructure ArgoCD Applications (Kustomize)
│ ├── base/ # Base Application manifests (upc-dev defaults) │ ├── base/ # Base Application manifests (one dir per component)
│ │ ├── kustomization.yaml │ │ ├── kustomization.yaml # Aggregates all component subdirectories
│ │ ├── traefik-application.yaml │ │ ├── traefik-application/
│ │ ├── keycloak.yaml │ │ ├── kustomization.yaml
│ │ ── grafana.yaml │ │ │ └── traefik-application.yaml
│ │ ├── gitea.yaml │ │ ├── keycloak/
│ │ ├── gitea-actions.yaml │ │ │ ├── kustomization.yaml
│ │ ├── tempo.yaml │ │ │ └── keycloak.yaml
│ │ ├── renovate.yaml │ │ ├── grafana/
│ │ ├── ... # All other Application manifests │ │ ├── prometheus/
│ │ ── secrets.yaml │ │ ── ... # Each component in its own subdirectory
├── overlays/ # Per-cluster overrides │ └── secrets/
│ ├── upc-dev/ # UpCloud Dev (uses base as-is) │ ├── overlays/ # Per-cluster Kustomize overrides
│ │ ── upc-prod/ # UpCloud Prod (patches value paths) │ │ ── upc-dev/ # UpCloud Dev — includes all (resources: ../../base)
│ │ ├── upc-prod/ # UpCloud Prod — all + patches
│ │ ├── aks-dev/ # Azure AKS Dev — selective components
│ │ ├── aks-prod/ # Azure AKS Prod
│ │ ├── eks-dev/ # AWS EKS Dev
│ │ ├── eks-prod/ # AWS EKS Prod
│ │ ├── gke-dev/ # GCP GKE Dev
│ │ └── gke-prod/ # GCP GKE Prod
│ ├── dashboards/ # Grafana dashboard ConfigMaps │ ├── dashboards/ # Grafana dashboard ConfigMaps
│ └── values/ # Helm value overrides for infra │ └── values/ # Helm value overrides for infra
│ ├── base/ # Shared values (all clusters) │ ├── base/ # Cloud-agnostic shared values
│ ├── traefik-values.yaml ├── upc-{dev,prod}/ # UpCloud: storage class, LB, pricing
│ ├── keycloak-values.yaml ├── aws-{dev,prod}/ # AWS: gp3, NLB, CUR pricing
│ ├── grafana-values.yaml ├── aks-{dev,prod}/ # Azure: managed-csi-premium, Standard LB
│ ├── prometheus-values.yaml └── gcp-{dev,prod}/ # GCP: premium-rwo, L4 LB
│ │ ├── gitea-values.yaml
│ │ └── ...
│ ├── upc-dev/ # upc-dev cluster-specific values
│ │ ├── traefik-values.yaml
│ │ ├── keycloak-values.yaml
│ │ └── grafana-values.yaml
│ └── upc-prod/ # upc-prod cluster-specific values
│ ├── traefik-values.yaml
│ ├── keycloak-values.yaml
│ └── grafana-values.yaml
├── apps/ # Business Application ArgoCD manifests (Kustomize) ├── apps/ # Business Application ArgoCD manifests (Kustomize)
│ ├── base/ # Base app manifests │ ├── base/ # One subdirectory per app
│ │ ├── kustomization.yaml │ │ ├── kustomization.yaml
│ │ ├── dot-ai-stack.yaml │ │ ├── musicman/
│ │ ── ... │ │ ── mcp10x/
│ │ ├── dot-ai-stack/
│ │ ├── ts-mcp/
│ │ └── argo-mcp/
│ └── overlays/ │ └── overlays/
│ ├── upc-dev/ # Uses base as-is │ ├── upc-dev/ # All apps (resources: ../../base)
── upc-prod/ # Patches value paths ── upc-prod/ # All apps + patches
│ └── aks-dev/ # Selective apps only
├── cluster-resources/ # Cluster-wide Kubernetes resources ├── cluster-resources/ # Cluster-wide Kubernetes resources
│ ├── ... │ ├── ...
@@ -175,6 +176,8 @@ launchpad/
**Key Points**: **Key Points**:
- `_app-of-apps-upc-dev.yaml` and `_app-of-apps-upc-prod.yaml` are the per-cluster root Applications - `_app-of-apps-upc-dev.yaml` and `_app-of-apps-upc-prod.yaml` are the per-cluster root Applications
- Each component in `base/` has its own subdirectory with a `kustomization.yaml`
- Overlays can include **all** components (`resources: [../../base]`) or **cherry-pick** specific ones (`resources: [../../base/grafana, ../../base/prometheus]`)
- Kustomize overlays in `infra/overlays/` render base Applications with per-cluster patches - Kustomize overlays in `infra/overlays/` render base Applications with per-cluster patches
- Helm values are split: `values/base/` (shared) + `values/upc-dev/` or `values/upc-prod/` (cluster-specific) - Helm values are split: `values/base/` (shared) + `values/upc-dev/` or `values/upc-prod/` (cluster-specific)
- `apps/` follows the same base/overlays pattern for business applications - `apps/` follows the same base/overlays pattern for business applications
@@ -287,7 +290,7 @@ app-repository/
### The App-of-Apps Pattern ### The App-of-Apps Pattern
``` ```
_app-of-apps-{upc-dev,upc-prod}.yaml (Root, per cluster) _app-of-apps-{cluster}.yaml (Root, per cluster — e.g. upc-dev, eks-prod, gke-dev)
├── infrastructure-apps (manages infra/) ├── infrastructure-apps (manages infra/)
│ ├── cluster-resources-application │ ├── cluster-resources-application
@@ -357,16 +360,30 @@ spec:
### Multi-Cluster Pattern ### Multi-Cluster Pattern
Kustomize overlays enable deploying the same Applications across clusters with different configurations: Kustomize overlays enable deploying the same Applications across clusters with different configurations.
Each component in `infra/base/` and `apps/base/` lives in its own subdirectory. Overlays define **which components to include** and optionally **patch** them:
```yaml ```yaml
# infra/base/ contains default (upc-dev) Applications # Option 1: Include ALL components (full cluster)
# Helm values are layered: base + cluster-specific # infra/overlays/upc-dev/kustomization.yaml
valueFiles: resources:
- $values/infra/values/base/traefik-values.yaml # Shared config - ../../base # Pulls in every component subdirectory
- $values/infra/values/upc-dev/traefik-values.yaml # Cluster-specific
# infra/overlays/upc-prod/kustomization.yaml patches the second valueFile # Option 2: Cherry-pick specific components (lightweight cluster)
# infra/overlays/aks-dev/kustomization.yaml
resources:
- ../../base/traefik-application
- ../../base/grafana
- ../../base/prometheus
- ../../base/loki
# Only listed components are deployed — others are excluded
```
Per-cluster patches swap Helm value file paths:
```yaml
# infra/overlays/upc-prod/kustomization.yaml
patches: patches:
- target: - target:
kind: Application kind: Application
@@ -377,6 +394,15 @@ patches:
value: $values/infra/values/upc-prod/traefik-values.yaml value: $values/infra/values/upc-prod/traefik-values.yaml
``` ```
Cloud-specific values (storage classes, load balancer annotations, cost model) are isolated in per-cluster value files. Base values are fully cloud-agnostic:
| Cloud | Storage Class | Load Balancer | OpenCost Provider |
|-------|--------------|---------------|-------------------|
| **UpCloud** | `upcloud-block-storage-maxiops` | UpCloud LB (ProxyProtocol v2) | Custom pricing |
| **AWS EKS** | `gp3` (EBS CSI) | NLB (ProxyProtocol v2) | AWS CUR |
| **Azure AKS** | `managed-csi-premium` | Standard LB (`externalTrafficPolicy: Local`) | Azure Billing API |
| **GCP GKE** | `premium-rwo` (PD CSI) | L4 passthrough NLB | GCP Cloud Billing |
**Benefits**: **Benefits**:
- Single source of truth for Application definitions - Single source of truth for Application definitions
- Cluster-specific values isolated per overlay - Cluster-specific values isolated per overlay
@@ -658,6 +684,6 @@ Notifications include:
--- ---
**Last Updated**: 2026-03-16 **Last Updated**: 2026-04-22
**Maintained By**: Platform Team **Maintained By**: Platform Team
**Questions?**: Contact #platform-support on Slack **Questions?**: Contact #platform-support on Slack

View File

@@ -37,7 +37,7 @@ Bootstrap a new cluster from scratch:
#### Prerequisites #### Prerequisites
1. **Kubernetes cluster running** (UpCloud or any K8s cluster) 1. **Kubernetes cluster running** (UpCloud, AWS EKS, Azure AKS, GCP GKE, or any K8s cluster)
2. **kubectl configured** with admin access 2. **kubectl configured** with admin access
3. **Repositories cloned** locally 3. **Repositories cloned** locally
@@ -54,11 +54,13 @@ kubectl get nodes
git clone https://git.forteapps.net/Forte/launchpad git clone https://git.forteapps.net/Forte/launchpad
cd launchpad cd launchpad
# 2. Set cluster name (optional) # 2. Run bootstrap script with cluster target
export CLUSTER_NAME="prod-cluster-01" # Available clusters: upc-dev, upc-prod, eks-dev, eks-prod,
# aks-dev, aks-prod, gke-dev, gke-prod
./bootstrap.sh upc-dev
# 3. Run bootstrap script # Cluster config is loaded from clusters/<cluster>.yaml
./bootstrap.sh # (cloudProvider, trustedIPs, domain, etc.)
``` ```
**What Happens:** **What Happens:**
@@ -186,13 +188,15 @@ Save the following file in private/ (gitignored) folder as secret.yaml
<paste your private key here> <paste your private key here>
project: default project: default
``` ```
Seal the secret using `kubeseal` command Write the secret to Vault:
```bash ```bash
kubeseal --format=yaml \ vault kv put kv/argocd/forte-helm-repo \
--namespace=argocd \ type=git \
< private/secret.yaml \ url=ssh://git@git.forteapps.net:2222/Forte/forte-helm.git \
> secrets/forte-helm-repo-secret-sealed.yaml sshPrivateKey="$(cat private/ssh-key)" \
project=default
``` ```
Then create a VaultStaticSecret CRD with `argocd.argoproj.io/secret-type: repository` label.
**Step 4: Register Repository in ArgoCD** **Step 4: Register Repository in ArgoCD**
@@ -497,7 +501,7 @@ See [Developer Guide](DEVELOPER-GUIDE.md#deploying-your-first-application) for d
**Quick checklist:** **Quick checklist:**
- [ ] Create `helm-prod-values/myapp/values.yaml` - [ ] Create `helm-prod-values/myapp/values.yaml`
- [ ] Create `apps/myapp.yaml` in config repo - [ ] Create `apps/myapp.yaml` in config repo
- [ ] Create SealedSecret if needed - [ ] Write secrets to Vault and create VaultStaticSecret CRD if needed
- [ ] Commit and push changes - [ ] Commit and push changes
- [ ] Verify sync in Slack/ArgoCD - [ ] Verify sync in Slack/ArgoCD
- [ ] Configure DNS for domain - [ ] Configure DNS for domain
@@ -668,92 +672,61 @@ db:
## Secret Management ## Secret Management
Secrets are managed via **HashiCorp Vault** and synced to Kubernetes by the **Vault Secrets Operator (VSO)**. See [Vault Secrets Operator Reference](vault-secrets-operator.md) for full details.
### Creating Secrets ### Creating Secrets
#### Step 1: Get Public Certificate #### Step 1: Write to Vault
```bash ```bash
# Fetch sealed-secrets public cert (one-time) # From literal values
kubeseal --fetch-cert \ vault kv put kv/myapp/myapp-credentials \
--controller-name=sealed-secrets-controller \ API_KEY=secret123 \
--controller-namespace=kube-system \ DB_PASSWORD=pass456
> pub-cert.pem
# Save this certificate for future use
``` ```
#### Step 2: Create Plain Secret #### Step 2: Create VaultStaticSecret CRD
```bash ```yaml
# Method 1: From literal values # apps/base/myapp/myapp-credentials-vault.yaml
kubectl create secret generic myapp-credentials \ apiVersion: secrets.hashicorp.com/v1beta1
--from-literal=API_KEY=secret123 \ kind: VaultStaticSecret
--from-literal=DB_PASSWORD=pass456 \ metadata:
--namespace=myapp \ name: myapp-credentials
--dry-run=client -o yaml > private/myapp-credentials.yaml namespace: myapp
spec:
# Method 2: From file type: kv-v2
kubectl create secret generic myapp-credentials \ mount: kv
--from-file=.env \ path: myapp/myapp-credentials
--namespace=myapp \ destination:
--dry-run=client -o yaml > private/myapp-credentials.yaml name: myapp-credentials
create: true
# Method 3: From multiple files refreshAfter: 30s
kubectl create secret generic myapp-credentials \ vaultAuthRef: vault-auth
--from-file=api-key.txt \
--from-file=db-password.txt \
--namespace=myapp \
--dry-run=client -o yaml > private/myapp-credentials.yaml
``` ```
#### Step 3: Seal Secret #### Step 3: Commit CRD
```bash ```bash
kubeseal --format=yaml \ git add apps/base/myapp/myapp-credentials-vault.yaml
--cert=pub-cert.pem \ git commit -m "Add myapp credentials (VSO)"
--namespace=myapp \
< private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml
```
#### Step 4: Commit Sealed Secret
```bash
git add secrets/myapp-credentials-sealed.yaml
git commit -m "Add myapp credentials"
git push git push
# Delete plain secret
rm private/myapp-credentials.yaml
``` ```
### Updating Secrets ArgoCD syncs the CRD, VSO creates the K8s Secret automatically.
### Updating / Rotating Secrets
**No git commit needed** — just update in Vault:
```bash ```bash
# 1. Create new version vault kv put kv/myapp/myapp-credentials \
kubectl create secret generic myapp-credentials \ API_KEY=new-secret-key \
--from-literal=API_KEY=new-secret-key \ DB_PASSWORD=new-password
--from-literal=DB_PASSWORD=new-password \
--namespace=myapp \
--dry-run=client -o yaml > private/myapp-credentials.yaml
# 2. Seal it # VSO picks up changes within 30 seconds
kubeseal --format=yaml \ # Restart pods if needed
--cert=pub-cert.pem \
--namespace=myapp \
< private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml
# 3. Commit
git add secrets/myapp-credentials-sealed.yaml
git commit -m "Update myapp credentials"
git push
# 4. Restart pods to pick up new secret
kubectl rollout restart deployment myapp -n myapp kubectl rollout restart deployment myapp -n myapp
# 5. Delete plain secret
rm private/myapp-credentials.yaml
``` ```
### Viewing Secrets (Unsealed) ### Viewing Secrets (Unsealed)
@@ -830,30 +803,13 @@ OIDC auth requires an `auth-oidc` Secret with two keys:
CLIENT_SECRET="your-oidc-client-secret-from-provider" CLIENT_SECRET="your-oidc-client-secret-from-provider"
COOKIE_SECRET=$(openssl rand -hex 32) COOKIE_SECRET=$(openssl rand -hex 32)
# Create plain secret # Write to Vault
kubectl create secret generic auth-oidc \ vault kv put kv/myapp/auth-oidc \
--from-literal=client-secret=$CLIENT_SECRET \ client-secret=$CLIENT_SECRET \
--from-literal=cookie-secret=$COOKIE_SECRET \ cookie-secret=$COOKIE_SECRET
--namespace=myapp \
--dry-run=client -o yaml > private/myapp-auth-oidc.yaml
# Seal it # Create VaultStaticSecret CRD (one-time) and commit
kubeseal --format=yaml \ # See docs/vault-secrets-operator.md for CRD template
--cert=pub-cert.pem \
--namespace=myapp \
< private/myapp-auth-oidc.yaml \
> secrets/myapp-auth-oidc-sealed.yaml
# Apply sealed secret
kubectl apply -f secrets/myapp-auth-oidc-sealed.yaml
# Commit to Git
git add secrets/myapp-auth-oidc-sealed.yaml
git commit -m "Add OIDC secrets for myapp"
git push
# Clean up
rm private/myapp-auth-oidc.yaml
``` ```
#### Rotating Authentication Secrets #### Rotating Authentication Secrets
@@ -880,16 +836,12 @@ kubectl rollout restart deployment myapp -n myapp
# Rotate cookie secret (safe - invalidates existing sessions) # Rotate cookie secret (safe - invalidates existing sessions)
NEW_COOKIE_SECRET=$(openssl rand -hex 32) NEW_COOKIE_SECRET=$(openssl rand -hex 32)
# Recreate secret # Update in Vault — no git commit needed
kubectl create secret generic auth-oidc \ vault kv put kv/myapp/auth-oidc \
--from-literal=client-secret=$CLIENT_SECRET \ client-secret=$CLIENT_SECRET \
--from-literal=cookie-secret=$NEW_COOKIE_SECRET \ cookie-secret=$NEW_COOKIE_SECRET
--namespace=myapp \
--dry-run=client -o yaml | \
kubeseal --format=yaml --cert=pub-cert.pem --namespace=myapp | \
kubectl apply -f -
# Restart to pick up new secret # VSO picks up within 30s. Restart pods to use new secret:
kubectl rollout restart deployment myapp -n myapp kubectl rollout restart deployment myapp -n myapp
``` ```
@@ -1262,13 +1214,21 @@ spec:
### Backup Strategy ### Backup Strategy
**Current State**: No automated backups **Current State**: Gitea daily backups to S3-compatible storage
**What Needs Backup**: **What Is Backed Up**:
- ❌ Cluster state (not backed up - recreate via GitOps) - ✅ Gitea repositories + database: Daily CronJob (`cluster-resources/gitea-backup-cronjob.yaml`) uploads to S3-compatible storage with 7-day retention
- ❌ Persistent volumes (currently not critical) - ✅ Git repositories: Full cluster config recoverable from Git
- ✅ Git repositories (Gitea provides backup) - ⚠️ Secrets: Sealed secrets in Git; unseal keys need safekeeping
- ⚠️ Secrets (sealed secrets in Git, unseal keys need safekeeping)
**What Is NOT Backed Up**:
- ❌ Cluster state (recreate via GitOps)
- ❌ Other persistent volumes (Prometheus, Loki, Tempo data)
**Per-cloud backup scripts** (manual restore helpers):
- UpCloud/AWS: `scripts/gitea-backup.sh` / `scripts/gitea-backup-eks.sh` (MinIO CLI, S3-compatible)
- Azure: `scripts/gitea-backup-aks.sh` (Azure CLI + Blob Storage)
- GCP: `scripts/gitea-backup-gke.sh` (gsutil + GCS)
### Cluster Rebuild ### Cluster Rebuild
@@ -1332,13 +1292,11 @@ kubectl get applications -n argocd -w
- pg_dump -U $DB_USER -d $DB_NAME > /backup/dump-$(date +%Y%m%d).sql - pg_dump -U $DB_USER -d $DB_NAME > /backup/dump-$(date +%Y%m%d).sql
``` ```
3. **Sealed Secrets private key backup** 3. **Vault backup**
```bash ```bash
# Backup sealed-secrets controller private key # Vault data is stored on PVC — ensure PVC snapshots are configured
kubectl get secret -n kube-system sealed-secrets-key \ # For disaster recovery, maintain Vault unseal keys in a secure location
-o yaml > sealed-secrets-key-backup.yaml # All secrets can be re-seeded from source if needed
# Store in secure location (password manager, vault)
``` ```
--- ---
@@ -1370,6 +1328,9 @@ kubectl get pods -n argocd
```bash ```bash
# UpCloud: Upgrade via control panel or CLI # UpCloud: Upgrade via control panel or CLI
# AWS EKS: eksctl upgrade cluster / AWS Console
# Azure AKS: az aks upgrade / Azure Portal
# GCP GKE: gcloud container clusters upgrade / Cloud Console
# After upgrade, verify cluster # After upgrade, verify cluster
kubectl version kubectl version
@@ -1507,18 +1468,35 @@ git push
### Multi-Cluster Setup ### Multi-Cluster Setup
The repository supports multiple clusters via Kustomize overlays: The repository supports multiple clusters across multiple clouds via Kustomize overlays:
**Active clusters:**
- **upc-dev** (default): `infra/overlays/upc-dev/` — uses base Applications as-is - **upc-dev** (default): `infra/overlays/upc-dev/` — uses base Applications as-is
- **upc-prod**: `infra/overlays/upc-prod/` — patches value file paths from `upc-dev` to `upc-prod` - **upc-prod**: `infra/overlays/upc-prod/` — patches value file paths from `upc-dev` to `upc-prod`
Each cluster has its own: **Cloud-ready templates (fill in `clusters/*.yaml` before use):**
- Root app-of-apps file: `_app-of-apps-upc-dev.yaml` / `_app-of-apps-upc-prod.yaml` - **eks-dev** / **eks-prod**: AWS EKS with NLB, gp3 storage, AWS CUR pricing
- Cluster-specific Helm values: `infra/values/upc-dev/` / `infra/values/upc-prod/` - **aks-dev** / **aks-prod**: Azure AKS with Standard LB, managed-csi-premium storage
- Sealed secrets: `secrets/upc-dev/` (others as needed) - **gke-dev** / **gke-prod**: GCP GKE with L4 LB, premium-rwo storage
- Apps overlay: `apps/overlays/upc-dev/` / `apps/overlays/upc-prod/`
To add a new cluster, create a new overlay directory (e.g., `infra/overlays/upc-staging/`) with patches that swap the value file paths. Each cluster has its own:
- Root app-of-apps: `_app-of-apps-{cluster}.yaml`
- Cluster config: `clusters/{cluster}.yaml` (domain, trustedIPs, cloudProvider)
- Kustomize overlay: `infra/overlays/{cluster}/kustomization.yaml`
- Helm value overrides: `infra/values/{cluster}/` (traefik, gitea, opencost)
- Sealed secrets: `secrets/{cluster}/` (as needed)
- Apps overlay: `apps/overlays/{cluster}/`
Cloud-specific values handled per-cluster:
| Concern | UpCloud | AWS EKS | Azure AKS | GCP GKE |
|---------|---------|---------|-----------|---------|
| **Storage class** | `upcloud-block-storage-maxiops` | `gp3` | `managed-csi-premium` | `premium-rwo` |
| **Load balancer** | UpCloud LB + ProxyProtocol v2 | NLB + ProxyProtocol v2 | Standard LB + `externalTrafficPolicy: Local` | L4 passthrough NLB |
| **Cost monitoring** | Custom pricing | AWS CUR | Azure Billing API | GCP Cloud Billing |
| **Backup storage** | UpCloud S3-compat | AWS S3 (native) | Azure Blob Storage | GCS |
To add a new cluster, create a new overlay directory (e.g., `infra/overlays/eks-staging/`) with patches that swap the value file paths, and a matching `clusters/eks-staging.yaml`.
### Blue-Green Deployments ### Blue-Green Deployments
@@ -1638,7 +1616,7 @@ echo "Remember to delete: $SECRET_FILE"
- [ ] Gitea Actions workflow configured - [ ] Gitea Actions workflow configured
- [ ] Helm values created in `helm-prod-values/` - [ ] Helm values created in `helm-prod-values/`
- [ ] ArgoCD application manifest created in `apps/` - [ ] ArgoCD application manifest created in `apps/`
- [ ] Secrets created and sealed - [ ] Secrets written to Vault and VaultStaticSecret CRD created
- [ ] DNS record added for domain - [ ] DNS record added for domain
- [ ] Application synced successfully - [ ] Application synced successfully
- [ ] Health check passed - [ ] Health check passed
@@ -1661,6 +1639,6 @@ echo "Remember to delete: $SECRET_FILE"
--- ---
**Last Updated**: 2026-03-16 **Last Updated**: 2026-04-22
**Maintained By**: Platform Team **Maintained By**: Platform Team
**Emergency Contact**: #platform-support on Slack **Emergency Contact**: #platform-support on Slack

View File

@@ -180,7 +180,7 @@ Reference for:
┌──────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ Kubernetes Clusters (UpCloud: upc-dev, upc-prod) │ Kubernetes Clusters (UpCloud, AWS, Azure, GCP)
│ ┌──────────────────────────────────────────────────────┐ │ │ ┌──────────────────────────────────────────────────────┐ │
│ │ Infrastructure: Traefik, Cert-Manager, Kyverno │ │ │ │ Infrastructure: Traefik, Cert-Manager, Kyverno │ │
│ ├──────────────────────────────────────────────────────┤ │ │ ├──────────────────────────────────────────────────────┤ │
@@ -194,7 +194,7 @@ Reference for:
### Key Technologies ### Key Technologies
- **GitOps**: ArgoCD - **GitOps**: ArgoCD
- **Kubernetes**: UpCloud Managed Kubernetes (multi-cluster: upc-dev, upc-prod) - **Kubernetes**: Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE)
- **Ingress**: Traefik v2 - **Ingress**: Traefik v2
- **Certificates**: Cert-Manager + Let's Encrypt - **Certificates**: Cert-Manager + Let's Encrypt
- **Policies**: Kyverno - **Policies**: Kyverno
@@ -299,11 +299,16 @@ docs/
## 🔄 Documentation Versions ## 🔄 Documentation Versions
**Current Version**: 1.0.0 **Current Version**: 1.0.0
**Last Updated**: 2026-03-16 **Last Updated**: 2026-04-22
**Maintained By**: Platform Team **Maintained By**: Platform Team
### Changelog ### Changelog
- **v1.1.0 (2026-04-22)**: Multi-cloud support
- Cloud-agnostic base values (storage, LB, pricing moved to per-cluster overlays)
- Added AWS EKS, Azure AKS, GCP GKE configurations
- Per-cloud backup scripts
- Updated all documentation
- **v1.0.0 (2026-03-16)**: Initial comprehensive documentation release - **v1.0.0 (2026-03-16)**: Initial comprehensive documentation release
- GitOps Architecture guide - GitOps Architecture guide
- Developer Onboarding guide - Developer Onboarding guide

View File

@@ -9,6 +9,7 @@
- [Kyverno Policies](#kyverno-policies) - [Kyverno Policies](#kyverno-policies)
- [Configuration Reference](#configuration-reference) - [Configuration Reference](#configuration-reference)
- [API Endpoints](#api-endpoints) - [API Endpoints](#api-endpoints)
- [Cloud Overlay Pattern](#cloud-overlay-pattern)
- [Glossary](#glossary) - [Glossary](#glossary)
--- ---
@@ -19,9 +20,10 @@
| Component | Value | | Component | Value |
|-----------|-------| |-----------|-------|
| **Provider** | UpCloud Managed Kubernetes | | **Provider** | Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE) |
| **Environment** | Production (internal use) | | **Environment** | Dev + Production per cloud |
| **Cluster Count** | Multi-cluster (upc-dev, upc-prod) | | **Active clusters** | UpCloud (upc-dev, upc-prod) |
| **Cloud-ready templates** | EKS, AKS, GKE (dev + prod each) |
| **GitOps Tool** | ArgoCD | | **GitOps Tool** | ArgoCD |
| **Ingress Controller** | Traefik v2 | | **Ingress Controller** | Traefik v2 |
| **Certificate Management** | Cert-Manager + Let's Encrypt | | **Certificate Management** | Cert-Manager + Let's Encrypt |
@@ -42,7 +44,7 @@ Internet
[DNS: *.forteapps.net] [DNS: *.forteapps.net]
[UpCloud LoadBalancer] [Cloud Load Balancer]
[Traefik Ingress Controller] [Traefik Ingress Controller]
@@ -74,40 +76,59 @@ launchpad/
├── _app-of-apps-upc-dev.yaml # Root ArgoCD Application (upc-dev) ├── _app-of-apps-upc-dev.yaml # Root ArgoCD Application (upc-dev)
├── _app-of-apps-upc-prod.yaml # Root ArgoCD Application (upc-prod) ├── _app-of-apps-upc-prod.yaml # Root ArgoCD Application (upc-prod)
├── infra/ # Infrastructure applications ├── infra/ # Infrastructure applications (Kustomize)
│ ├── cluster-resources-application.yaml │ ├── base/ # One subdirectory per component
│ ├── enterprise-apps.yaml │ ├── kustomization.yaml # Aggregates all component subdirectories
│ ├── traefik-application.yaml │ ├── traefik-application/
├── cert-manager-application.yaml │ │ ├── kustomization.yaml
├── kyverno.yaml │ │ └── traefik-application.yaml
│ ├── kyverno-policies.yaml │ ├── keycloak/
├── prometheus.yaml │ │ ├── kustomization.yaml
├── grafana.yaml │ │ └── keycloak.yaml
│ ├── loki.yaml │ ├── grafana/
│ ├── tempo.yaml │ ├── prometheus/
│ ├── fluent-bit.yaml │ ├── loki/
│ ├── trivy.yaml │ ├── tempo/
│ ├── gitea.yaml │ ├── gitea/
│ ├── gitea-actions.yaml │ ├── opencost/
│ ├── sealedsecrets.yaml │ ├── ... # Each component in own directory
── secrets.yaml │ └── secrets/
│ ├── renovate.yaml │ ├── overlays/ # Per-cluster: include all or cherry-pick
│ │ ├── upc-dev/ # resources: [../../base] (all components)
│ │ ├── upc-prod/ # resources: [../../base] + patches
│ │ ├── aks-dev/ # resources: [../../base/grafana, ...] (selective)
│ │ └── .../ # 8 clusters total
│ └── values/ │ └── values/
│ ├── argocd-values.yaml │ ├── base/ # Cloud-agnostic Helm values
├── prometheus-values.yaml │ ├── gitea-values.yaml
│ │ ├── opencost-values.yaml
│ │ ├── prometheus-values.yaml
│ │ └── ...
│ ├── upc-dev/ # UpCloud dev overlay values
│ │ ├── traefik-values.yaml
│ │ ├── keycloak-values.yaml
│ │ ├── grafana-values.yaml
│ │ ├── gitea-values.yaml
│ │ └── opencost-values.yaml
│ └── upc-prod/ # UpCloud prod overlay values
│ ├── traefik-values.yaml
│ ├── keycloak-values.yaml
│ ├── grafana-values.yaml │ ├── grafana-values.yaml
│ ├── loki-values.yaml
│ ├── tempo-values.yaml
│ ├── gitea-values.yaml │ ├── gitea-values.yaml
├── gitea-actions-values.yaml └── opencost-values.yaml
│ ├── fluent-bit-values.yaml
│ └── renovate-values.yaml
├── apps/ # Business applications ├── apps/ # Business applications (Kustomize)
│ ├── mcp10x.yaml │ ├── base/ # One subdirectory per app
│ ├── musicman.yaml │ ├── kustomization.yaml
│ ├── dot-ai-stack.yaml │ ├── musicman/
└── argo-mcp.yaml │ ├── mcp10x/
│ │ ├── dot-ai-stack/
│ │ ├── ts-mcp/
│ │ └── argo-mcp/
│ └── overlays/ # Per-cluster: include all or cherry-pick
│ ├── upc-dev/
│ ├── upc-prod/
│ └── aks-dev/ # Selective apps only
├── cluster-resources/ # Cluster-level resources ├── cluster-resources/ # Cluster-level resources
│ ├── cert-manager-namespace.yaml │ ├── cert-manager-namespace.yaml
@@ -128,12 +149,39 @@ launchpad/
│ └── auth-sidecar-injector.yaml │ └── auth-sidecar-injector.yaml
├── secrets/ # Application secrets (sealed) ├── secrets/ # Application secrets (sealed)
│ ├── argocd-mcp-credentials.yaml │ ├── base/ # All SealedSecrets (shared across clouds)
│ ├── dot-ai-secrets.yaml │ ├── kustomization.yaml
│ ├── gitea-credentials-sealed.yaml │ ├── argocd-forte-helm-secret-sealed.yaml
│ ├── gitea-runner-token-sealed.yaml │ ├── argocd-mcp-credentials.yaml
│ ├── mcp10x-credentials-sealed.yaml │ ├── argocdmcp-auth-oidc-sealed.yaml
└── musicman-credentials.yaml │ ├── dot-ai-secrets.yaml
│ │ ├── forte10x-app-credentials-sealed.yaml
│ │ ├── gitea-backup-s3-sealed.yaml
│ │ ├── gitea-credentials-sealed.yaml
│ │ ├── gitea-runner-token-sealed.yaml
│ │ ├── gitea-smtp-secret-sealed.yaml
│ │ ├── keycloak-credentials-sealed.yaml
│ │ ├── musicman-auth-oidc-sealed.yaml
│ │ ├── musicman-credentials.yaml
│ │ └── renovate-env-sealed.yaml
│ └── overlays/ # Per-cloud overlays (reference base)
│ ├── aks-dev/kustomization.yaml
│ ├── aks-prod/kustomization.yaml
│ ├── eks-dev/kustomization.yaml
│ ├── eks-prod/kustomization.yaml
│ ├── gke-dev/kustomization.yaml
│ ├── gke-prod/kustomization.yaml
│ ├── upc-dev/kustomization.yaml
│ └── upc-prod/kustomization.yaml
├── scripts/ # Operational helper scripts
│ ├── gitea-backup.sh # S3 backup helper (list/download)
│ ├── gitea-restore.sh
│ └── backup/ # Per-cloud backup reference scripts
│ ├── s3-minio.sh # S3-compatible (UpCloud, MinIO, Wasabi)
│ ├── aws-s3.sh # Native AWS S3
│ ├── azure-blob.sh # Azure Blob Storage
│ └── gcp-gcs.sh # GCP Cloud Storage
├── private/ # Local-only (Git-ignored) ├── private/ # Local-only (Git-ignored)
│ ├── *.yaml │ ├── *.yaml
@@ -602,10 +650,134 @@ retry:
4. 40 seconds 4. 40 seconds
5. 80 seconds (capped at 3 minutes) 5. 80 seconds (capped at 3 minutes)
### Global Settings (`argocd-cm`)
| Setting | Value | Purpose |
|---------|-------|---------|
| `application.resourceTrackingMethod` | `annotation` | Track resources via annotations |
| `timeout.reconciliation` | `60s` | Reconciliation interval |
| `admin.enabled` | `false` | Admin login disabled (SSO-only) |
| `url` | `https://argocd.forteapps.net` | External URL for ArgoCD UI |
**Git Submodule Disable**: Set via `configs.params` (NOT `repoServer.env` — that causes strategic merge conflicts with chart's `valueFrom` entries):
```yaml
configs:
params:
"reposerver.enable.git.submodule": "false"
```
This writes to `argocd-cmd-params-cm` ConfigMap, which the chart already reads via `valueFrom`. Submodules (e.g., `shared-prompts`) are not needed for K8s manifest generation.
**Break-Glass Admin Access**: Admin login is disabled (`admin.enabled: false`). The admin password remains in `argocd-secret`. To re-enable temporarily:
```bash
# Enable admin login
kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"true"}}'
# Log in as admin, do what's needed, then disable again
kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"false"}}'
```
ArgoCD picks up ConfigMap changes within the reconciliation timeout (60s). Note: ArgoCD will revert this on next sync — this is intentional (temporary access only).
**OIDC Authentication** (Keycloak):
```yaml
configs:
cm:
oidc.config: |
name: Forte SSO
issuer: https://id.forteapps.net/realms/forte
clientID: argocd
clientSecret: $oidc.clientSecret
requestedScopes: ["openid", "email", "profile"]
rbacConfig:
policy.csv: |
g, ArgoCD Admins, role:admin
g, ArgoCD Viewers, role:readonly
# Deny users not in any declared KC group
policy.default: ""
scopes: '[groups]'
```
**Access Control**: Only users in Keycloak groups `ArgoCD Admins` or `ArgoCD Viewers` can access ArgoCD. Users not in either group are denied (empty `policy.default`). Assign users to groups in Keycloak admin console.
- ArgoCD does NOT add `openid` implicitly — must include in `requestedScopes`
- Do NOT add `groups` as a scope — the KC groups mapper emits the claim regardless
- `$oidc.clientSecret` references the `oidc.clientSecret` key in `argocd-secret`
- OIDC secret is synced by CronJob `argocd-oidc-sync` (see `cluster-resources/argocd-oidc-secret-sync.yaml`)
- The CronJob bridges `argocd-oidc-credentials` (from KC registrar) → `argocd-secret` every 2 min
- Safe for fresh deploys: no-ops if source secret doesn't exist yet
**Ingress** (Traefik + TLS):
```yaml
server:
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls: true
extraArgs:
- --insecure
configs:
params:
"server.insecure": true
```
TLS terminates at Traefik; ArgoCD runs in insecure mode behind the proxy.
--- ---
## Infrastructure Components ## Infrastructure Components
### Homepage (Platform Dashboard)
**Chart**: `jameswynn/homepage`
**Namespace**: `homepage`
**URL**: `https://start.forteapps.net`
Platform dashboard that auto-discovers deployed apps via Kubernetes service annotations.
**Discovery mechanism**: Services annotated with `gethomepage.dev/enabled: "true"` appear in the dashboard. Apps not deployed = annotations absent = not shown. Fully dynamic per environment.
**Annotated services**:
| Service | Namespace | Group | Widget |
|---------|-----------|-------|--------|
| `gitea-http` | `gitea` | DevOps | `gitea` |
| `argocd-server` | `argocd` | DevOps | `argocd` |
| `keycloak` | `keycloak` | Identity | none |
| `grafana` | `monitoring` | Monitoring | `grafana` |
| `karpor-server` | `karpor` | DevOps | none |
**Adding a new app**: Annotate the app's Service in its Helm values:
```yaml
service:
annotations:
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "My App"
gethomepage.dev/description: "What it does"
gethomepage.dev/group: "GroupName"
gethomepage.dev/icon: "icon-name" # https://github.com/walkxcode/dashboard-icons
gethomepage.dev/href: "https://myapp.forteapps.net"
# Optional live widget:
gethomepage.dev/widget.type: "myapp"
gethomepage.dev/widget.url: "https://myapp.forteapps.net"
# gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_MYAPP_TOKEN}}"
```
**Widget API credentials**: Inject via env vars into the Homepage pod:
```yaml
# In homepage-values.yaml per environment
env:
- name: HOMEPAGE_VAR_GRAFANA_TOKEN
valueFrom:
secretKeyRef:
name: homepage-widget-credentials
key: grafana-token
```
Then reference as `gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_GRAFANA_TOKEN}}"`.
**Values files**:
- `infra/values/base/homepage-values.yaml` — RBAC, kubernetes mode, layout
- `infra/values/{env}/homepage-values.yaml` — hostname per environment
---
### Traefik ### Traefik
**Chart**: `traefik/traefik` **Chart**: `traefik/traefik`
@@ -677,6 +849,10 @@ spec:
**Chart**: `sealed-secrets/sealed-secrets-controller` **Chart**: `sealed-secrets/sealed-secrets-controller`
**Namespace**: `kube-system` **Namespace**: `kube-system`
**Directory Structure**: `secrets/base/` contains all SealedSecrets with a `kustomization.yaml`. Per-cloud overlays in `secrets/overlays/<cloud>/` reference the base via Kustomize. The ArgoCD `secrets` Application points to the active overlay (e.g., `secrets/overlays/upc-dev`), and `infra/overlays/upc-prod` patches the path to `secrets/overlays/upc-prod`.
To add cloud-specific secrets, create a new SealedSecret in the overlay directory and add it to the overlay's `kustomization.yaml`.
**Public Certificate**: **Public Certificate**:
```bash ```bash
kubeseal --fetch-cert \ kubeseal --fetch-cert \
@@ -717,6 +893,15 @@ kubeStateMetrics:
- Loki - Loki
- Tempo - Tempo
**Ingress**: Exposed via Traefik at `https://grafana.forteapps.net` with cert-manager TLS.
**OIDC Authentication** (Keycloak):
- Uses `grafana.ini.auth.generic_oauth` with KC `grafana` client
- Secret `grafana-oidc-credentials` synced by KC registrar, loaded via `envFromSecrets`
- SSO-only mode: `auth.disable_login_form: true` + `auth.generic_oauth.auto_login: true`
- Role mapping via JMESPath on `resource_access.grafana.roles` claim (requires KC client role mapper)
- Roles: KC client roles `Admin`/`Editor` map to Grafana roles; default is `Viewer`
### Loki ### Loki
**Chart**: `grafana/loki-stack` **Chart**: `grafana/loki-stack`
@@ -819,6 +1004,8 @@ postgresql:
**Email Notifications**: Enabled (`ENABLE_NOTIFY_MAIL: true`). SMTP credentials injected via `gitea-smtp-secret` using `additionalConfigFromEnvs` with `GITEA__mailer__USER` / `GITEA__mailer__PASSWD` environment variables. **Email Notifications**: Enabled (`ENABLE_NOTIFY_MAIL: true`). SMTP credentials injected via `gitea-smtp-secret` using `additionalConfigFromEnvs` with `GITEA__mailer__USER` / `GITEA__mailer__PASSWD` environment variables.
**Auto-Watch**: Disabled (`AUTO_WATCH_ON_CHANGES: false`, `AUTO_WATCH_NEW_REPOS: false`). Prevents contributors from being auto-subscribed to repo notifications on push, reducing email noise from CI bots (e.g., ai-review PR comments). Users who were already watching before this change need to manually unwatch or switch to "Only participating".
**Endpoints**: **Endpoints**:
- Web UI: `https://git.forteapps.net` - Web UI: `https://git.forteapps.net`
- SSH: port 22 (ClusterIP) - SSH: port 22 (ClusterIP)
@@ -876,6 +1063,84 @@ dind:
- Gitea admin panel (`/admin/runners`) — runners show as Online - Gitea admin panel (`/admin/runners`) — runners show as Online
- Create test workflow in `.gitea/workflows/test.yml` — job executes - Create test workflow in `.gitea/workflows/test.yml` — job executes
### AI Code Review (ai-review)
**Type**: Gitea Actions workflow (`.gitea/workflows/ai-review.yaml`)
**Trigger**: `pull_request` events (`opened`, `synchronize`)
**Runner**: `ubuntu-latest` (container: `nikitafilonov/ai-review:latest`)
**Purpose**: Automated AI-powered code review on pull requests using Claude (Anthropic). Posts inline comments on changed lines and a PR summary comment highlighting infrastructure impact.
**Architecture**:
- Uses [xai-review](https://github.com/nicktechnologies/xai-review) Docker image
- Shared configuration and prompts live in the `shared-prompts` Git submodule (→ `Forte/ai-review-prompts`)
- Review mode: `ONLY_ADDED_WITH_CONTEXT` — reviews only new/changed lines plus surrounding context (token-efficient)
- Agent mode: disabled (one-shot review, no multi-turn reasoning)
- LLM: Claude Sonnet (`claude-sonnet-4-20250514`)
**Shared Prompts Structure** (submodule: `Forte/ai-review-prompts`):
```
shared-prompts/
base/
security.md # org-wide security rules (all profiles)
iac/
.ai-review.yaml # IaC/GitOps profile config
inline.md # inline review prompt
summary.md # PR summary prompt
# future profiles: backend/, frontend/, etc.
```
**Configuration** (`shared-prompts/iac/.ai-review.yaml`):
```yaml
llm:
provider: CLAUDE
model: claude-sonnet-4-20250514
vcs:
provider: GITEA
review:
mode: ONLY_ADDED_WITH_CONTEXT
agent:
enabled: false
prompt:
inline_prompt_files: # concatenated in order
- ./shared-prompts/base/security.md
- ./shared-prompts/iac/inline.md
summary_prompt_files:
- ./shared-prompts/iac/summary.md
ignore:
- "*.sealed.yaml"
- "*.lock"
- "docs/**"
```
**Custom Prompts** (IaC profile):
- `shared-prompts/base/security.md` — org-wide security rules, concatenated before every inline review prompt
- `shared-prompts/iac/inline.md` — IaC-specific inline review (YAML, Helm, K8s manifests, shell scripts), max 7 comments
- `shared-prompts/iac/summary.md` — PR summary: affected services/namespaces, infrastructure impact, security flags
**Prompt composition**: ai-review does not support Jinja includes. Instead, list multiple files under `inline_prompt_files` / `summary_prompt_files` — they are concatenated in order with double newlines.
**Adding a new profile**: Create a new directory (e.g., `backend/`) with its own `.ai-review.yaml`, `inline.md`, and `summary.md`. The `inline_prompt_files` list should include `base/security.md` first, then the profile-specific prompt. Reference it in the consuming repo's workflow: `AI_REVIEW_CONFIG_FILE_YAML=./shared-prompts/backend/.ai-review.yaml`
**Required Secrets** (configure in Gitea repo or org settings):
| Secret | Purpose |
|--------|---------|
| `ANTHROPIC_API_KEY` | Claude API key (from Anthropic console) |
| `AI_REVIEW_TOKEN` | Gitea API token with `write:repository` + `read:repository` scopes (use a bot/service account) |
**Setup Steps**:
1. Create a Gitea bot/service account and generate an API token with `write:repository` + `read:repository` scopes
2. Add `AI_REVIEW_TOKEN` secret in Gitea repo settings → Actions → Secrets
3. Add `ANTHROPIC_API_KEY` secret with your Anthropic API key
4. Ensure the `shared-prompts` submodule is initialized (`git submodule update --init`)
5. Push the workflow file — it triggers automatically on PR creation/update
**Verification**:
- Open a PR with infrastructure changes → workflow runs → inline comments + summary appear
- Check Gitea Actions tab for workflow run status and logs
- Monitor Anthropic usage dashboard for token consumption
### Keycloak Client Registrar ### Keycloak Client Registrar
**Type**: CronJob (deployed via Keycloak Helm chart `extraDeploy`) **Type**: CronJob (deployed via Keycloak Helm chart `extraDeploy`)
@@ -989,6 +1254,33 @@ kubectl get secret keycloak-client-<app> -n keycloak -o jsonpath='{.metadata.ann
**See**: [Developer Guide - Adding a New Keycloak Client](DEVELOPER-GUIDE.md#adding-a-new-keycloak-client) **See**: [Developer Guide - Adding a New Keycloak Client](DEVELOPER-GUIDE.md#adding-a-new-keycloak-client)
### Karpor
**Chart**: `karpor` from `https://kusionstack.github.io/charts`
**Version**: 0.7.6 (app v0.6.4)
**Namespace**: `karpor`
**Sync Wave**: 1
**Purpose**: Kubernetes visualization and intelligence tool. Provides cross-cluster resource search, compliance checking, and topology visualization. Gives platform engineers a unified view of all cluster resources and their relationships.
**Architecture** (4 components):
- **Server** — main Karpor API/UI (port 7443)
- **Syncer** — syncs cluster state into the search index
- **ElasticSearch** — search backend for resource indexing
- **etcd** — persistent key-value store (10Gi PVC)
**Configuration** (`infra/values/base/karpor-values.yaml`):
- `namespaceEnabled: false` — ArgoCD manages namespace creation
- Default resource limits tuned for small clusters
- ElasticSearch: 2 CPU / 4Gi memory (the heaviest component)
- AI features available but not enabled (requires `server.ai.authToken` + backend config)
**Access**: Port-forward to reach the UI:
```bash
kubectl port-forward svc/karpor-release-server -n karpor 7443:7443
# Open https://localhost:7443
```
### Renovate ### Renovate
**Chart**: `renovate` (OCI: `ghcr.io/renovatebot/charts`) **Chart**: `renovate` (OCI: `ghcr.io/renovatebot/charts`)
@@ -1092,6 +1384,46 @@ spec:
- Adds source tracking annotations (`keycloak.forteapps.net/source-namespace`, `keycloak.forteapps.net/source-name`) - Adds source tracking annotations (`keycloak.forteapps.net/source-namespace`, `keycloak.forteapps.net/source-name`)
- `synchronize: true` — changes to the source Secret are reflected in the clone - `synchronize: true` — changes to the source Secret are reflected in the clone
### Keycloak Microsoft/Entra Identity Provider
**File**: `infra/values/upc-dev/keycloak-values.yaml`
**Namespace**: `keycloak`
**Purpose**: Configures Microsoft Entra (Azure AD) as an external identity provider for the Forte realm, enabling SSO via Microsoft accounts with token storage for downstream API access (e.g., Microsoft Graph).
**Configuration via keycloakConfigCli**:
- IdP alias: `forte-entra`, provider: `microsoft`
- Client secret injected from `microsoft-idp-credentials` Secret via `$(env:MS_IDP_CLIENT_SECRET)` syntax
- `extraEnvVarsSecret: microsoft-idp-credentials` makes the Secret available as env vars to config-cli
**Key Configuration Notes**:
| Field | Location | Notes |
|-------|----------|-------|
| `tenant` | `config.tenant` | **Must be `tenant`, NOT `tenantId`** — wrong key silently falls back to `common` (multi-tenant) |
| `storeToken` | Top-level IdP field | **NOT inside `config`** — enables broker token storage for KC broker API |
| `defaultScope` | `config.defaultScope` | Space-separated: `openid email profile User.Read Mail.Send` |
| `syncMode` | `config.syncMode` | `IMPORT` — imports user on first login |
**Token Storage & Broker Access**:
- `storeToken: true` persists the Entra access token in Keycloak
- Realm role `default-roles-forte` includes composite `broker.read-token` — grants all realm users access to broker token API
- Broker token retrievable via: `GET /realms/forte/broker/forte-entra/token`
**Identity Provider Mappers**:
- `forte-entra-email`: Hardcodes `emailVerified=true` for Entra-authenticated users (Entra guarantees email verification)
**Required Secret** (`microsoft-idp-credentials`):
```yaml
apiVersion: v1
kind: Secret
metadata:
name: microsoft-idp-credentials
namespace: keycloak
stringData:
MS_IDP_CLIENT_SECRET: "<entra-app-client-secret>"
```
### Default Namespace Blocker ### Default Namespace Blocker
**File**: `cluster-resources/policies/default-ns-blocker.yaml` **File**: `cluster-resources/policies/default-ns-blocker.yaml`
@@ -1436,7 +1768,23 @@ Forward to Application (localhost:3000)
Application processes request Application processes request
``` ```
**See**: [Developer Guide - Enabling Authentication](DEVELOPER-GUIDE.md#enabling-authentication-for-applications) for usage examples. #### Forwarded Headers
After successful authentication, the sidecar injects user identity as HTTP headers before forwarding the request to the application container:
| Header | Description | Auth Modes |
|--------|-------------|------------|
| `X-Auth-User` | Username or display name | Token, OIDC, MCP |
| `X-Auth-Email` | User email address | OIDC |
| `X-Auth-Subject` | OIDC `sub` claim (stable user ID) | OIDC, MCP |
| `X-Auth-Groups` | Comma-separated group memberships | OIDC (if `groups` scope) |
| `X-Auth-Token` | The validated access token | All modes |
These headers are trustworthy because the auto-generated `NetworkPolicy` restricts pod ingress to the sidecar port only — external traffic cannot reach the application container directly, so headers cannot be spoofed.
Applications should read these headers to obtain authenticated user information (e.g. for display, authorisation decisions, or audit logging) instead of implementing their own authentication.
**See**: [Developer Guide - Accessing Authenticated User Information](DEVELOPER-GUIDE.md#accessing-authenticated-user-information) for code examples.
--- ---
@@ -1470,14 +1818,22 @@ Recommended resource allocation:
### Storage Classes ### Storage Classes
Default storage class used: **UpCloud default** (varies by provider) Storage classes are cloud-specific and configured in per-cluster value overrides (`infra/values/{cluster}/gitea-values.yaml`):
| Cloud | Storage Class | Driver |
|-------|--------------|--------|
| **UpCloud** | `upcloud-block-storage-maxiops` | UpCloud CSI |
| **AWS EKS** | `gp3` | EBS CSI |
| **Azure AKS** | `managed-csi-premium` | Azure Disk CSI |
| **GCP GKE** | `premium-rwo` | PD CSI |
```yaml ```yaml
# Example: base values omit storageClass (set in per-cluster overlay)
persistence: persistence:
enabled: true enabled: true
storageClass: "" # Uses default
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 5Gi size: 5Gi
# storageClass set by infra/values/{cluster}/gitea-values.yaml
``` ```
--- ---
@@ -1541,6 +1897,88 @@ POST /loki/api/v1/push
--- ---
## Cloud Overlay Pattern
### Overview
Cloud-specific configuration (StorageClass, LoadBalancer annotations, pricing models, etc.) lives in per-cloud overlay value files, **not** in `base/`. Adding a new cloud provider only requires a new overlay directory — no base changes.
### Supported Clouds
| Cloud | Dev overlay | Prod overlay | StorageClass | LB type |
|-------|-----------|-------------|-------------|---------|
| **UpCloud** | `upc-dev` | `upc-prod` | `upcloud-block-storage-maxiops` | UpCloud LB (proxy protocol v2) |
| **Azure AKS** | `aks-dev` | `aks-prod` | `managed-csi-premium` | Azure LB |
| **AWS EKS** | `eks-dev` | `eks-prod` | `gp3` | AWS NLB (proxy protocol) |
| **GCP GKE** | `gke-dev` | `gke-prod` | `premium-rwo` | GCP NEG |
Bootstrap any cluster with: `./bootstrap.sh <cluster>` (e.g., `./bootstrap.sh aks-dev`)
### How It Works
Each ArgoCD Application uses **multi-source Helm values** with two value files:
```yaml
# infra/base/gitea.yaml (example)
helm:
valueFiles:
- $values/infra/values/base/gitea-values.yaml # [0] cloud-agnostic
- $values/infra/values/upc-dev/gitea-values.yaml # [1] cloud-specific (default: upc-dev)
```
The `upc-prod` Kustomize overlay patches index `[1]` to swap the cloud-specific file:
```yaml
# infra/overlays/upc-prod/kustomization.yaml
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/upc-prod/gitea-values.yaml
```
### Components Using Cloud Overlays
| Component | Cloud-specific config | Overlay value file |
|-----------|----------------------|-------------------|
| **Traefik** | LB annotations, proxy protocol IPs | `traefik-values.yaml` |
| **Keycloak** | Hostname, TLS settings | `keycloak-values.yaml` |
| **Grafana** | Hostname, datasource URLs | `grafana-values.yaml` |
| **Gitea** | StorageClass (persistence + PostgreSQL) | `gitea-values.yaml` |
| **OpenCost** | Custom pricing model (CPU/RAM/storage rates) | `opencost-values.yaml` |
### Backup CronJob
The `gitea-backup` CronJob uses a generic `s3` alias for `minio/mc`. The actual endpoint and credentials come from the `gitea-backup-s3` Sealed Secret, which is per-cloud. Reference scripts for different cloud providers are in `scripts/backup/`:
| Script | Provider | Tool |
|--------|----------|------|
| `s3-minio.sh` | S3-compatible (UpCloud, MinIO, Wasabi) | `minio/mc` |
| `aws-s3.sh` | AWS S3 | `aws` CLI |
| `azure-blob.sh` | Azure Blob Storage | `az` CLI |
| `gcp-gcs.sh` | GCP Cloud Storage | `gsutil` |
### Adding a New Cloud Provider
To add support for a new cloud (e.g., `oci-dev` for Oracle Cloud):
1. **Cluster config**: `clusters/oci-dev.yaml` — clusterName, domain, trustedIPs, cloudProvider
2. **Overlay value files** in `infra/values/oci-dev/`:
- `traefik-values.yaml` — LB annotations, proxy protocol config
- `keycloak-values.yaml` — hostname
- `grafana-values.yaml` — hostname
- `gitea-values.yaml``storageClass` for persistence + PostgreSQL
- `opencost-values.yaml` — pricing model or cloud billing integration
3. **Kustomize overlay**: `infra/overlays/oci-dev/kustomization.yaml` — patch `valueFiles[1]` for each Application
4. **App-of-apps**: `_app-of-apps-oci-dev.yaml` — points to `infra/overlays/oci-dev`
5. **Secrets overlay**: `secrets/overlays/oci-dev/kustomization.yaml` — references `../../base`, add cloud-specific SealedSecrets if needed
6. **Secrets patch**: Add patch to `infra/overlays/oci-dev/kustomization.yaml` to swap secrets path to `secrets/overlays/oci-dev`
7. **Bootstrap**: `./bootstrap.sh oci-dev`
---
## Glossary ## Glossary
### Terms ### Terms
@@ -1673,6 +2111,6 @@ team: platform
--- ---
**Last Updated**: 2026-04-16 **Last Updated**: 2026-04-22
**Maintained By**: Platform Team **Maintained By**: Platform Team
**Version**: 1.0.0 **Version**: 1.0.0

View File

@@ -0,0 +1,206 @@
# Vault Secrets Operator (VSO) Reference
## Overview
The platform uses HashiCorp Vault Secrets Operator (VSO) to sync secrets from Vault KV v2 to native Kubernetes Secrets. This replaces the previous SealedSecrets workflow.
**Key benefit**: Secret values can be rotated via Vault UI/CLI without a git commit. Only new VaultStaticSecret CRDs need to be committed.
## Architecture
```
Vault (KV v2) VSO K8s Secret
kv/{namespace}/{name} --> VaultStaticSecret CRD --> Secret in namespace
(polls every 30s)
```
- **Vault**: Standalone instance in `vault` namespace, KV v2 at `kv/`
- **VSO**: Deployed in `vault-secrets-operator-system` namespace via ArgoCD
- **Auth**: Kubernetes auth method — each namespace has its own ServiceAccount + VaultAuth CRD
## KV Path Convention
```
kv/{namespace}/{secret-name}
```
Examples:
- `kv/homepage/homepage-widget-credentials`
- `kv/argocd/forte-helm-repo`
- `kv/gitea/gitea-smtp-secret`
- `kv/keycloak/keycloak-credentials`
## Vault Policy Structure
Each namespace gets a read-only policy:
```hcl
# Policy: ns-{namespace}
path "kv/data/{namespace}/*" {
capabilities = ["read"]
}
path "kv/metadata/{namespace}/*" {
capabilities = ["read", "list"]
}
```
## Kubernetes Auth Roles
Each namespace has a bound ServiceAccount:
```
Role: ns-{namespace}
bound_service_account_names: vault-auth-{namespace}
bound_service_account_namespaces: {namespace}
policies: ns-{namespace}
audience: vault
ttl: 1h
```
## CRD Reference
### VaultAuth
Per-namespace auth binding. One per namespace.
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: {namespace}
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-{namespace}
serviceAccount: vault-auth-{namespace}
audiences:
- vault
```
Each VaultAuth requires a corresponding ServiceAccount:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-{namespace}
namespace: {namespace}
```
### VaultStaticSecret
One per secret. Syncs a Vault KV path to a K8s Secret.
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: {secret-name}
namespace: {namespace}
spec:
type: kv-v2
mount: kv
path: {namespace}/{secret-name}
destination:
name: {secret-name} # K8s Secret name (must match what apps expect)
create: true
type: Opaque # Optional, defaults to Opaque
labels: # Optional, for secrets that need labels
some-label: "value"
refreshAfter: 30s
vaultAuthRef: vault-auth
```
## Special Labels
Some secrets require specific labels for correct operation:
| Secret | Label | Purpose |
|--------|-------|---------|
| `renovate-env` | `allowedToBeCloned: "true"` | Kyverno secret-cloner policy |
| `gitea-smtp-secret` | `allowedToBeCloned: "true"` | Kyverno secret-cloner policy |
| `forte-helm-repo` | `argocd.argoproj.io/secret-type: repository` | ArgoCD repository recognition |
| `forte10x-repo-creds` | `argocd.argoproj.io/secret-type: repository` | ArgoCD repository recognition |
| `mcp10x-repo-creds` | `argocd.argoproj.io/secret-type: repository` | ArgoCD repository recognition |
These are set in `destination.labels` of the VaultStaticSecret CRD.
## Namespaces & Secrets Map
| Namespace | Secrets |
|-----------|---------|
| `homepage` | homepage-widget-credentials |
| `renovate` | renovate-env |
| `gitea` | gitea-credentials, gitea-backup-s3, gitea-smtp-secret, gitea-runner-token |
| `keycloak` | keycloak-credentials, microsoft-idp-credentials (overlay) |
| `argocd` | forte-helm-repo, forte10x-repo-creds, mcp10x-repo-creds, argocd-notifications-secret |
| `mcp10x` | app-credentials |
| `ts-mcp` | ts-mcp-secrets |
| `argocd-mcp` | auth-oidc, argocd-mcp-credentials |
| `dot-ai` | dot-ai-secrets |
| `music-man` | musicman-credentials |
## Common Operations
### Add a new secret
1. Write to Vault:
```bash
vault kv put kv/{namespace}/{secret-name} key1=val1 key2=val2
```
2. Create VaultStaticSecret YAML (see template above)
3. Add to kustomization.yaml in the appropriate directory
4. Commit and push — ArgoCD syncs the CRD, VSO creates the K8s Secret
### Rotate a secret value
No git commit needed:
```bash
vault kv put kv/{namespace}/{secret-name} key1=new-val1 key2=new-val2
```
VSO picks up changes within 30 seconds.
### Check sync status
```bash
# VaultAuth status
kubectl get vaultauth -n {namespace}
# VaultStaticSecret status
kubectl get vaultstaticsecret -n {namespace}
# Verify K8s Secret exists with correct keys
kubectl get secret {name} -n {namespace} -o jsonpath='{.data}' | jq
```
### Troubleshooting
1. **VaultAuth not authenticating**: Check ServiceAccount exists, Vault role matches SA name/namespace
2. **VaultStaticSecret not syncing**: Check `kubectl describe vaultstaticsecret {name} -n {ns}` for events
3. **Secret missing keys**: Verify Vault KV path has all expected keys: `vault kv get kv/{ns}/{name}`
4. **Permission denied**: Verify Vault policy allows read on `kv/data/{ns}/*`
## File Locations
| Type | Location |
|------|----------|
| VSO ArgoCD Application | `infra/base/vault-secrets-operator/` |
| VSO Helm values | `infra/values/base/vault-secrets-operator-values.yaml` |
| Vault policies script | `scripts/vault-setup-policies.sh` |
| Seed script | `scripts/seed-vault-from-cluster.sh` |
| VaultAuth + VaultStaticSecret | Alongside ArgoCD Application in each component directory |
## Setup Scripts
```bash
# Create all Vault policies and auth roles
./scripts/vault-setup-policies.sh
# Seed Vault KV from existing K8s Secrets
./scripts/seed-vault-from-cluster.sh
```

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cert-manager-application.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cluster-resources-application.yaml

View File

@@ -0,0 +1,42 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: databunker
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
labels:
app.kubernetes.io/name: databunker
app.kubernetes.io/part-of: identity
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: https://securitybunker.github.io/databunkerpro-setup
chart: databunkerpro
targetRevision: "0.1.0"
helm:
releaseName: databunkerpro
valueFiles:
- $values/infra/values/base/databunker-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: databunker
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- databunker.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- enterprise-apps.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- fluent-bit.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gitea-actions.yaml

View File

@@ -0,0 +1,15 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: gitea-backup-s3
namespace: gitea
spec:
type: kv-v2
mount: kv
path: gitea/gitea-backup-s3
destination:
name: gitea-backup-s3
create: true
type: Opaque
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: gitea-credentials
namespace: gitea
spec:
type: kv-v2
mount: kv
path: gitea/gitea-credentials
destination:
name: gitea-credentials
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: gitea-runner-token
namespace: gitea
spec:
type: kv-v2
mount: kv
path: gitea/gitea-runner-token
destination:
name: gitea-runner-token
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -0,0 +1,17 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: gitea-smtp-secret
namespace: gitea
spec:
type: kv-v2
mount: kv
path: gitea/gitea-smtp-secret
destination:
name: gitea-smtp-secret
create: true
type: Opaque
labels:
allowedToBeCloned: "true"
refreshAfter: 30s
vaultAuthRef: vault-auth

View File

@@ -22,6 +22,7 @@ spec:
releaseName: gitea releaseName: gitea
valueFiles: valueFiles:
- $values/infra/values/base/gitea-values.yaml - $values/infra/values/base/gitea-values.yaml
- $values/infra/values/upc-dev/gitea-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD targetRevision: HEAD

View File

@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gitea.yaml
- vault-auth.yaml
- gitea-credentials-vault.yaml
- gitea-backup-s3-vault.yaml
- gitea-smtp-secret-vault.yaml
- gitea-runner-token-vault.yaml
# Removed: gitea-*-sealed.yaml (migrated to VSO)

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-gitea
namespace: gitea
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: gitea
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-gitea
serviceAccount: vault-auth-gitea
audiences:
- vault

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- grafana-dashboards.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- grafana.yaml

Some files were not shown because too many files have changed in this diff Show More