Technical Reference¶
Table of Contents¶
- Architecture Components
- Repository Reference
- Helm Chart Reference
- ArgoCD Configuration
- Infrastructure Components
- Kyverno Policies
- Configuration Reference
- API Endpoints
- Glossary
Architecture Components¶
Cluster Specifications¶
| Component | Value |
|---|---|
| Provider | UpCloud Managed Kubernetes |
| Environment | Production (internal use) |
| Cluster Count | Multi-cluster (upc-dev, upc-prod) |
| GitOps Tool | ArgoCD |
| Ingress Controller | Traefik v2 |
| Certificate Management | Cert-Manager + Let's Encrypt |
| Policy Engine | Kyverno |
| Secret Management | Sealed Secrets (Bitnami) |
| Monitoring | Prometheus + Grafana |
| Logging | Loki + Fluent-Bit |
| Tracing | Tempo (OTLP) |
| Container Scanning | Trivy |
| Version Control | Gitea |
Network Architecture¶
Internet
│
▼
[DNS: *.forteapps.net]
│
▼
[UpCloud LoadBalancer]
│
▼
[Traefik Ingress Controller]
│
├──► IngressRoute (TLS termination via Cert-Manager)
│
├──► Service (ClusterIP)
│ │
│ └──► Pod (Application Container)
│
└──► Service (Database - ClusterIP)
│
└──► StatefulSet (PostgreSQL)
Repository Reference¶
Config Repository: launchpad¶
URL: https://git.forteapps.net/Forte/launchpad
Directory Structure¶
launchpad/
├── bootstrap.sh # Cluster initialization script
├── _app-of-apps-upc-dev.yaml # Root ArgoCD Application (upc-dev)
├── _app-of-apps-upc-prod.yaml # Root ArgoCD Application (upc-prod)
│
├── infra/ # Infrastructure applications
│ ├── cluster-resources-application.yaml
│ ├── enterprise-apps.yaml
│ ├── traefik-application.yaml
│ ├── cert-manager-application.yaml
│ ├── kyverno.yaml
│ ├── kyverno-policies.yaml
│ ├── prometheus.yaml
│ ├── grafana.yaml
│ ├── loki.yaml
│ ├── tempo.yaml
│ ├── fluent-bit.yaml
│ ├── trivy.yaml
│ ├── gitea.yaml
│ ├── gitea-actions.yaml
│ ├── sealedsecrets.yaml
│ ├── secrets.yaml
│ ├── renovate.yaml
│ └── values/
│ ├── argocd-values.yaml
│ ├── prometheus-values.yaml
│ ├── grafana-values.yaml
│ ├── loki-values.yaml
│ ├── tempo-values.yaml
│ ├── gitea-values.yaml
│ ├── gitea-actions-values.yaml
│ ├── fluent-bit-values.yaml
│ └── renovate-values.yaml
│
├── apps/ # Business applications
│ ├── mcp10x.yaml
│ ├── musicman.yaml
│ ├── dot-ai-stack.yaml
│ └── argo-mcp.yaml
│
├── cluster-resources/ # Cluster-level resources
│ ├── cert-manager-namespace.yaml
│ ├── secrets-namespace.yaml
│ ├── letsencrypt-issuer.yaml
│ ├── kyverno-config.yaml
│ ├── argocd-notifications-secret-sealed.yaml
│ ├── forte10x-repo-credentials-sealed.yaml
│ ├── mcp10x-repo-credentials-sealed.yaml
│ └── policies/
│ ├── deployment-verifier.yaml
│ ├── label-checker.yaml
│ ├── bare-pod-cleaner.yaml
│ ├── replicaset-cleaner.yaml
│ ├── default-ns-blocker.yaml
│ ├── secret-cloner.yaml
│ ├── keycloak-client-cloner.yaml
│ └── auth-sidecar-injector.yaml
│
├── secrets/ # Application secrets (sealed)
│ ├── argocd-mcp-credentials.yaml
│ ├── dot-ai-secrets.yaml
│ ├── gitea-credentials-sealed.yaml
│ ├── gitea-runner-token-sealed.yaml
│ ├── mcp10x-credentials-sealed.yaml
│ └── musicman-credentials.yaml
│
├── private/ # Local-only (Git-ignored)
│ ├── *.yaml
│ └── *.sh
│
└── docs/ # Documentation
├── GITOPS-ARCHITECTURE.md
├── DEVELOPER-GUIDE.md
├── OPERATIONS-RUNBOOK.md
└── REFERENCE.md
Key Files¶
bootstrap.sh
#!/bin/zsh
# Initializes cluster with ArgoCD
ArgoCd() {
helm upgrade --install argocd argo-cd \
--repo https://argoproj.github.io/argo-helm \
--namespace argocd --create-namespace \
--values infra/values/base/argocd-values.yaml \
--set notifications.context.clusterName="$CLUSTER_NAME" \
--timeout 60s --atomic
kubectl apply -f _app-of-apps-upc-dev.yaml -n argocd # or _app-of-apps-upc-prod.yaml
}
_app-of-apps-upc-dev.yaml / _app-of-apps-upc-prod.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
path: infra
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
Helm Charts Repository: forte-helm¶
URL: https://github.com/fortedigital/forte-helm
Chart: forteapp¶
Version: 0.1.0 App Version: 1.0.0 Type: application
Templates¶
| Template | Purpose |
|---|---|
_helpers.tpl |
Template helper functions |
namespace.yaml |
Namespace resource |
deployment.yaml |
Main application Deployment |
service.yaml |
ClusterIP Service |
ingressroute.yaml |
Traefik IngressRoute |
certificate.yaml |
Cert-Manager Certificate |
configmap.yaml |
Application ConfigMap |
secret-auth-tokens.yaml |
Authentication tokens |
hpa.yaml |
Horizontal Pod Autoscaler |
database-statefulset.yaml |
Optional PostgreSQL StatefulSet |
database-service.yaml |
PostgreSQL Service |
Default Values Schema¶
app:
image:
repository: "" # Required
tag: "" # Required
pullPolicy: IfNotPresent
containerPort: 3000
replicaCount: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
hpa:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
extraEnv: []
# - name: KEY
# value: "value"
envSecretName: "" # Reference to Secret
nodeEnv: production
db:
enabled: false
name: postgres
image:
repository: postgres
tag: "16-alpine"
service:
type: ClusterIP
port: 5432
targetPort: 5432
persistence:
enabled: true
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
extraEnv: []
envSecretName: ""
livenessProbe:
exec:
command:
- pg_isready
- -U
- db_user
- -d
- db_name
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- db_user
- -d
- db_name
initialDelaySeconds: 5
periodSeconds: 5
service:
type: ClusterIP
port: 3000
ingress:
enabled: false
host: ""
entrypoint: websecure
tls:
enabled: true
secretName: ""
clusterIssuer: letsencrypt-prod
auth:
enabled: false # Enable authentication sidecar injection
type: token # Authentication mode: "token" or "oidc"
# Token-based authentication configuration
tokens: [] # List of valid bearer tokens (hex strings, 32+ bytes recommended)
# - d4f88f6d9292c10cc3e21c4aad56d2be485db532b54fe961d738e1137d247823
# - 8803f621acc3898df1d7a8f514bc3602551a0681a8f747bd4e43c3c5849d57a7
# OIDC authentication configuration
oidc:
authority: "" # OIDC provider URL (e.g., https://auth.example.com/realms/master)
clientId: "" # OIDC client ID registered with provider
scopes: "openid,profile,email" # OAuth scopes (comma-separated)
callbackPath: /auth/callback # OAuth callback path (default: /auth/callback)
# Note: Client secret must be in 'auth-oidc' Secret (client-secret key)
# Cookie secret must be in 'auth-oidc' Secret (cookie-secret key)
configmap: [] # Application ConfigMap key-value pairs
# KEY: value
# DB_HOST: postgres
# DB_PORT: "5432"
Helm Values Repository: helm-values¶
URL: https://github.com/fortedigital/helm-values.git
Structure¶
helm-values/
├── mcp10x/
│ └── values.yaml
├── musicman/
│ └── values.yaml
├── mcpcoder/
│ └── values.yaml
└── argocd-mcp/
└── values.yaml
Example: mcp10x/values.yaml¶
app:
image:
repository: ghcr.io/fortedigital/10x
tag: 2.0.4 # Updated by CI/CD
extraEnv:
- name: PORT
value: "3000"
- name: SKILLS_DIR
value: "/app/skills"
- name: FLOWCASE_ENDPOINT
value: "https://forte.cvpartner.com/api/"
envSecretName: "app-credentials"
auth:
enabled: false
tokens:
- d4f88f6d9292c10cc3e21c4aad56d2be485db532b54fe961d738e1137d247823
ingress:
enabled: true
host: mcp10x.forteapps.net
Helm Chart Reference¶
Template Functions¶
forteapp.fullname¶
forteapp.labels¶
{{ include "forteapp.labels" . }}
# Output:
# app.kubernetes.io/name: forteapp
# app.kubernetes.io/instance: <release-name>
# app.kubernetes.io/version: <chart-version>
# app.kubernetes.io/managed-by: Helm
forteapp.selectorLabels¶
{{ include "forteapp.selectorLabels" . }}
# Output:
# app.kubernetes.io/name: forteapp
# app.kubernetes.io/instance: <release-name>
Deployment Specification¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "forteapp.fullname" . }}
labels:
{{- include "forteapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.app.replicaCount }}
selector:
matchLabels:
{{- include "forteapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
policies.forteapps.io/auth: {{ .Values.auth.enabled | quote }}
labels:
{{- include "forteapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: app
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.app.image.containerPort }}
env:
- name: NODE_ENV
value: {{ .Values.app.nodeEnv | quote }}
{{- with .Values.app.extraEnv }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.app.envSecretName }}
envFrom:
- secretRef:
name: {{ .Values.app.envSecretName }}
{{- end }}
resources:
{{- toYaml .Values.app.resources | nindent 10 }}
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
IngressRoute Specification¶
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: {{ include "forteapp.fullname" . }}
spec:
entryPoints:
- {{ .Values.ingress.entrypoint }}
routes:
- match: Host(`{{ .Values.ingress.host }}`)
kind: Rule
services:
- name: {{ include "forteapp.fullname" . }}
port: {{ .Values.service.port }}
{{- if .Values.ingress.tls.enabled }}
tls:
secretName: {{ default .Release.Name .Values.ingress.tls.secretName }}-tls
{{- end }}
Certificate Specification¶
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "forteapp.fullname" . }}-tls
spec:
secretName: {{ default .Release.Name .Values.ingress.tls.secretName }}-tls
issuerRef:
name: {{ .Values.ingress.tls.clusterIssuer }}
kind: ClusterIssuer
dnsNames:
- {{ .Values.ingress.host }}
ArgoCD Configuration¶
Application Manifest Schema¶
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: <app-name>
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
notifications.argoproj.io/subscribe.on-degraded.slack: ""
labels:
app.kubernetes.io/name: <app-name>
app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
# Multi-source configuration
sources:
- repoURL: https://github.com/fortedigital/forte-helm
path: forteapp
targetRevision: HEAD
helm:
valueFiles:
- $values/<app-name>/values.yaml
- repoURL: git@github.com:fortedigital/helm-values.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: <app-name>
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true
- Replace=false
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
Sync Waves¶
| Wave | Components | Purpose |
|---|---|---|
-1 |
Namespaces | Create namespaces first |
0 |
Kyverno | Install policy engine |
1 |
Cluster resources, infrastructure | Base infrastructure |
2+ |
Applications | Business applications |
Sync Options¶
| Option | Description |
|---|---|
CreateNamespace=true |
Automatically create target namespace |
Validate=true |
Validate resources before applying |
ServerSideApply=true |
Use server-side apply (safer) |
Replace=false |
Don't use kubectl replace |
Prune=true |
Delete resources not in Git |
Retry Policy¶
retry:
limit: 5 # Max retry attempts
backoff:
duration: 5s # Initial backoff
factor: 2 # Exponential factor
maxDuration: 3m # Max backoff time
Retry Schedule: 1. 5 seconds 2. 10 seconds 3. 20 seconds 4. 40 seconds 5. 80 seconds (capped at 3 minutes)
Infrastructure Components¶
Traefik¶
Chart: traefik/traefik
Version: Latest
Namespace: traefik
Configuration:
# infra/base/traefik-application.yaml
replicas: 2
service:
type: LoadBalancer
ingressRoute:
dashboard:
enabled: false
ports:
web:
redirectTo: websecure # HTTP → HTTPS redirect
websecure:
tls:
enabled: true
Endpoints:
- HTTP: :80 → Redirects to HTTPS
- HTTPS: :443
Cert-Manager¶
Chart: jetstack/cert-manager
Namespace: cert-manager
ClusterIssuer:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@forteapps.net
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: traefik
Kyverno¶
Chart: kyverno/kyverno
Namespace: kyverno
Policies: - Secret cloner - Default namespace blocker - Bare pod cleaner - ReplicaSet cleaner - Deployment verifier - Auth sidecar injector
Sealed Secrets¶
Chart: sealed-secrets/sealed-secrets-controller
Namespace: kube-system
Public Certificate:
kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
> pub-cert.pem
Prometheus¶
Chart: prometheus-community/prometheus
Namespace: monitoring
Configuration:
server:
persistentVolume:
enabled: true
size: 10Gi
alertmanager:
enabled: false
nodeExporter:
enabled: true
kubeStateMetrics:
enabled: true
Grafana¶
Chart: grafana/grafana
Namespace: monitoring
Datasources: - Prometheus - Loki - Tempo
Loki¶
Chart: grafana/loki-stack
Namespace: monitoring
Configuration:
Tempo¶
Chart: grafana/tempo
Version: 1.24.4
Namespace: monitoring
Purpose: Distributed tracing backend receiving OTLP traces from Traefik and other instrumented services.
Configuration:
tempo:
storage:
trace:
backend: local
local:
path: /var/tempo/traces
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
persistence:
enabled: true
size: 10Gi
Endpoints:
- gRPC OTLP receiver: :4317
- HTTP OTLP receiver: :4318
- Query API: :3200
Grafana Integration: - Trace-to-logs correlation with Loki (by namespace, pod, container) - Trace-to-metrics correlation with Prometheus (by service name) - Service graph and node graph visualization
Fluent-Bit¶
Chart: fluent/fluent-bit
Namespace: monitoring
Output: Loki
Gitea¶
Chart: gitea/gitea
Version: 12.5.0 (app v1.25.4)
Namespace: gitea
Purpose: Self-hosted Git repository hosting with pull requests, issues, CI/CD (Gitea Actions), container registry, and package registry.
Configuration:
# infra/base/gitea.yaml + infra/values/base/gitea-values.yaml
ingress:
host: git.forteapps.net
tls: cert-manager (letsencrypt-prod)
gitea:
admin:
existingSecret: gitea-credentials
config:
service:
DISABLE_REGISTRATION: true
ALLOW_ONLY_EXTERNAL_REGISTRATION: true
actions:
ENABLED: true
packages:
ENABLED: true
metrics:
ENABLED: true
postgresql:
enabled: true
persistence: 8Gi (upcloud-block-storage-maxiops)
Authentication: Keycloak OIDC via forte realm (client ID: gitea). Protocol mapper: email_verified hardcoded claim (true, boolean) on ID token, Access token, and Userinfo.
Endpoints:
- Web UI: https://git.forteapps.net
- SSH: port 22 (ClusterIP)
- Metrics: /metrics (Prometheus scrape)
Secrets: gitea-credentials (SealedSecret) containing admin-password, postgres-password, secret (OIDC client secret)
Gitea Actions Runners¶
Chart: actions (from https://dl.gitea.com/charts)
Namespace: gitea
Sync Wave: 2 (deploys after Gitea)
Purpose: Act runners execute Gitea Actions CI/CD workflows. Deployed as a StatefulSet with a Docker-in-Docker sidecar for container-based job execution.
Configuration:
# infra/base/gitea-actions.yaml + infra/values/base/gitea-actions-values.yaml
replicaCount: 3
runner:
labels:
- "ubuntu-latest:docker://node:20-bookworm"
- "ubuntu-22.04:docker://node:20-bookworm"
existingSecret: gitea-runner-token
gitea:
instance:
url: http://gitea-http.gitea.svc.cluster.local:3000
dind:
enabled: true # Docker-in-Docker sidecar (privileged)
Resources:
| Container | CPU Request | Memory Request | CPU Limit | Memory Limit |
|---|---|---|---|---|
| Runner | 250m | 256Mi | 1 | 1Gi |
| DinD sidecar | 250m | 256Mi | 1 | 1Gi |
Secrets: gitea-runner-token (SealedSecret) containing token (instance-level runner registration token from /admin/runners)
Setup Steps:
1. Get runner registration token from Gitea admin panel (/admin/runners)
2. Fill in private/gitea-runner-token.yaml with the token
3. Seal: kubeseal --format yaml < private/gitea-runner-token.yaml > secrets/gitea-runner-token-sealed.yaml
4. Commit and push — ArgoCD deploys runners automatically
Verification:
- kubectl get statefulset -n gitea — 3/3 runners ready
- Gitea admin panel (/admin/runners) — runners show as Online
- Create test workflow in .gitea/workflows/test.yml — job executes
Keycloak Client Registrar¶
Type: CronJob (deployed via Keycloak Helm chart extraDeploy)
Namespace: keycloak
Schedule: */2 * * * * (every 2 minutes)
Purpose: Handles two responsibilities:
1. Legacy sync — extracts secrets from Keycloak clients with k8s.secret.sync: "true" attribute (same as former PostSync syncer)
2. Self-service registration — processes config Secrets (cloned by Kyverno) to register new OIDC clients and sync their credentials
How It Works:
Legacy path (existing clients like Gitea):
1. Authenticates to Keycloak Admin API using admin credentials from keycloak-credentials secret
2. Queries all clients in the forte realm
3. Filters clients with k8s.secret.sync: "true" attribute
4. For each matching client, retrieves the auto-generated secret via Keycloak Admin API
5. Creates/updates a K8s Secret in the target namespace (from k8s.secret.namespace attribute)
6. Always writes a central copy to the secrets namespace
Self-service path (new clients):
1. Lists Secrets in keycloak namespace with label keycloak.forteapps.net/client-config=true
2. For each config Secret, parses client.json and computes a config hash
3. Skips if hash matches annotation and credential Secret already exists
4. Creates or updates the Keycloak client via Admin API
5. Fetches the generated client secret
6. Upserts credential Secret in target namespace + central secrets namespace
7. Annotates config Secret with sync status, config hash, and timestamp
Resources:
- ServiceAccount: keycloak-client-registrar (namespace: keycloak)
- ClusterRole: keycloak-client-registrar (secrets: get/list/create/update/patch; namespaces: get/list)
- ClusterRoleBinding: keycloak-client-registrar
- CronJob: keycloak-client-registrar
Kyverno Policy: keycloak-client-config-cloner — clones labeled Secrets from app namespaces to keycloak namespace (see Kyverno Policies)
Legacy Client Attributes (set in forte-realm.json):
| Attribute | Required | Default | Description |
|---|---|---|---|
k8s.secret.sync |
Yes | — | Set to "true" to enable syncing |
k8s.secret.namespace |
Yes | — | Target K8s namespace |
k8s.secret.name |
Yes | — | Name of the K8s Secret |
k8s.secret.client-id-key |
No | client-id |
Field name for client ID in the Secret |
k8s.secret.client-secret-key |
No | client-secret |
Field name for client secret in the Secret |
Self-Service Config Secret Schema:
apiVersion: v1
kind: Secret
metadata:
name: keycloak-client-<app>
namespace: <app-namespace>
labels:
keycloak.forteapps.net/client-config: "true"
stringData:
client.json: |
{
"clientId": "<app>",
"name": "<App Name>",
"redirectUris": ["https://<app>.forteapps.net/*"],
"webOrigins": ["https://<app>.forteapps.net"],
"defaultClientScopes": ["openid", "email", "profile"],
"protocolMappers": [],
"secret": {
"namespace": "<app-namespace>",
"name": "<app>-oidc-credentials",
"keys": { "clientId": "client-id", "clientSecret": "client-secret" }
}
}
Created Credential Secret Format:
apiVersion: v1
kind: Secret
metadata:
name: <target-name>
namespace: <target-namespace>
labels:
app.kubernetes.io/managed-by: keycloak-client-registrar
type: Opaque
data:
<client-id-key>: <base64-encoded client ID>
<client-secret-key>: <base64-encoded client secret>
Config Secret Annotations (set by registrar):
| Annotation | Description |
|---|---|
keycloak.forteapps.net/config-hash |
SHA-256 hash of client.json for change detection |
keycloak.forteapps.net/sync-status |
synced or error |
keycloak.forteapps.net/last-sync |
ISO 8601 timestamp of last successful sync |
Verification:
# Check CronJob status
kubectl get cronjobs -n keycloak
# View latest registrar logs
kubectl logs -n keycloak job/$(kubectl get jobs -n keycloak --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}')
# Verify created secret
kubectl get secret <name> -n <namespace> -o yaml
# Check config Secret annotations (self-service)
kubectl get secret keycloak-client-<app> -n keycloak -o jsonpath='{.metadata.annotations}'
See: Developer Guide - Adding a New Keycloak Client
Renovate¶
Chart: renovate (OCI: ghcr.io/renovatebot/charts)
Version: 46.109.0 (app v43.113.0)
Namespace: renovate
Sync Wave: 2
Purpose: Automated dependency update bot. Runs as a CronJob that scans Gitea repositories for outdated dependencies and creates pull requests with updates.
Configuration:
# infra/base/renovate.yaml + infra/values/base/renovate-values.yaml
cronjob:
schedule: "@daily"
concurrencyPolicy: Forbid
renovate:
config:
platform: gitea
endpoint: https://git.forteapps.net
autodiscover: true
gitAuthor: "Renovate Bot <renovate@forteapps.net>"
packageRules:
- matchRepositories: ["**/10x"]
assignees: ["edvard.unsvag"]
reviewers: ["edvard.unsvag"]
- matchRepositories: ["**/auth-sidecar"]
assignees: ["danijel.simeunovic"]
reviewers: ["danijel.simeunovic"]
- matchRepositories: ["**/forte-helm"]
assignees: ["danijel.simeunovic"]
reviewers: ["danijel.simeunovic"]
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: "2", memory: 4Gi }
Note: Assignees and reviewers are only applied at PR creation time. Existing PRs must be closed and recreated for new assignment rules to take effect.
Secrets: renovate-env (SealedSecret in secrets namespace, cloned by Kyverno) containing:
- RENOVATE_TOKEN — Gitea PAT with repo write + issue write permissions
- RENOVATE_GITHUB_COM_TOKEN — GitHub PAT (public_repo read-only) for changelog fetching
Setup Steps:
1. Fill in private/renovate-env.yaml with tokens
2. Seal: kubeseal --format yaml < private/renovate-env.yaml > secrets/renovate-env-sealed.yaml
3. Commit and push — ArgoCD deploys the CronJob, Kyverno clones the secret
Verification:
- kubectl get cronjob -n renovate — CronJob exists
- kubectl create job --from=cronjob/renovate renovate-test -n renovate — manual trigger
- kubectl logs -n renovate job/renovate-test — check logs
Gitea Pages¶
Purpose: Hosts the MkDocs documentation site for this repository.
How It Works:
- A Gitea Actions workflow (.gitea/workflows/docs.yaml) builds MkDocs on push to main
- The built site is force-pushed to the gitea-pages branch
- Gitea serves the static site from that branch
URL: https://git.forteapps.net/Forte/launchpad/pages/
Configuration:
- Gitea server config: ENABLE_GITEA_PAGES: true (in gitea-values.yaml)
- MkDocs config: mkdocs.yml (repo root)
- Source files: docs/ directory
- Theme: Material for MkDocs
Trigger Paths:
- docs/**
- mkdocs.yml
- Dockerfile.docs
- nginx.conf
Kyverno Policies¶
Secret Cloner¶
File: cluster-resources/policies/secret-cloner.yaml
Purpose: Automatically clone secrets from secrets namespace to new namespaces
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sync-secret-with-multi-clone
spec:
rules:
- name: clone-secret
match:
any:
- resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: Secret
name: "{{ request.object.metadata.name }}"
namespace: "{{ request.object.metadata.name }}"
synchronize: true
clone:
namespace: secrets
name: shared-credentials
Label Requirement: Secrets must have allowedToBeCloned: "true"
Keycloak Client Config Cloner¶
File: cluster-resources/policies/keycloak-client-cloner.yaml
Purpose: Clones Secrets labeled keycloak.forteapps.net/client-config: "true" from app namespaces to the keycloak namespace. This allows apps to declare their OIDC client configuration in their own namespace, which the Keycloak Client Registrar then processes.
Trigger: Any Secret with label keycloak.forteapps.net/client-config: "true" created outside the keycloak namespace.
Behavior:
- Generates a copy of the Secret in the keycloak namespace with the same 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
Default Namespace Blocker¶
File: cluster-resources/policies/default-ns-blocker.yaml
Purpose: Prevent resources from being created in default namespace
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-default-namespace
spec:
validationFailureAction: enforce
rules:
- name: validate-namespace
match:
any:
- resources:
kinds:
- Pod
- Deployment
- Service
validate:
message: "Using 'default' namespace is not allowed"
pattern:
metadata:
namespace: "!default"
Bare Pod Cleaner¶
File: cluster-resources/policies/bare-pod-cleaner.yaml
Purpose: Delete pods without ownerReferences (not managed by Deployment/StatefulSet)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: cleanup-bare-pods
spec:
rules:
- name: delete-bare-pod
match:
any:
- resources:
kinds:
- Pod
preconditions:
all:
- key: "{{ request.object.metadata.ownerReferences[] || '' }}"
operator: Equals
value: ""
validate:
message: "Bare pods (without controllers) are not allowed"
deny: {}
Auth Sidecar Injector¶
File: cluster-resources/policies/auth-sidecar-injector.yaml
Purpose: Automatically inject authentication sidecar into pods with authentication enabled
Rules: 6 rules in the policy
1. generate-auth-tokens-secret - Creates Secret for token mode
2. generate-auth-oidc-secret - Creates Secret for OIDC mode
3. inject-sidecar-token - Injects auth sidecar for token mode
4. inject-sidecar-oidc - Injects auth sidecar for OIDC mode
5. inject-sidecar-mcp - Injects auth sidecar for MCP OAuth mode (RFC 9728 / RFC 7591)
6. generate-auth-network-policy - Creates NetworkPolicy to restrict ingress
Trigger Annotation¶
Authentication Modes¶
Token Mode (default):
# Annotations
policies.forteapps.io/auth: "true"
policies.forteapps.io/auth-type: "token"
policies.forteapps.io/auth-token-secret-name: "auth-tokens"
policies.forteapps.io/auth-upstream-url: "http://localhost:3000"
# Optional customization
policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar"
policies.forteapps.io/auth-image-version: "latest"
OIDC Mode:
# Annotations (required)
policies.forteapps.io/auth: "true"
policies.forteapps.io/auth-type: "oidc"
policies.forteapps.io/auth-oidc-authority: "https://auth.example.com/realms/master"
policies.forteapps.io/auth-oidc-client-id: "myapp"
# Optional annotations
policies.forteapps.io/auth-oidc-callback-path: "/auth/callback"
policies.forteapps.io/auth-oidc-scopes: "openid,profile,email"
policies.forteapps.io/auth-upstream-url: "http://localhost:3000"
policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar"
policies.forteapps.io/auth-image-version: "latest"
MCP Mode (OAuth 2.0 for MCP servers, implements RFC 9728 / RFC 7591):
# Annotations (required)
policies.forteapps.io/auth: "true"
policies.forteapps.io/auth-type: "mcp"
policies.forteapps.io/auth-mcp-resource: "https://mcp.example.com"
policies.forteapps.io/auth-mcp-authority: "https://auth.example.com"
# Optional annotations
policies.forteapps.io/auth-mcp-scopes: "read,write"
policies.forteapps.io/auth-upstream-url: "http://localhost:3000"
policies.forteapps.io/auth-log-level: "info"
policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar"
policies.forteapps.io/auth-image-version: "latest"
Sidecar Container Specification¶
Token Mode:
name: authn
image: ghcr.io/fortedigital/auth-sidecar:latest
ports:
- containerPort: 8080
name: auth
protocol: TCP
env:
- name: AUTH_MODE
value: "token"
- name: AUTH_LISTEN_ADDR
value: ":8080"
- name: AUTH_UPSTREAM_URL
value: "http://localhost:3000"
- name: AUTH_TOKEN_FILE
value: "/etc/auth/tokens"
volumeMounts:
- name: auth-tokens
mountPath: /etc/auth
readOnly: true
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 50m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
OIDC Mode:
name: authn
image: ghcr.io/fortedigital/auth-sidecar:latest
ports:
- containerPort: 8080
name: auth
protocol: TCP
env:
- name: AUTH_MODE
value: "oidc"
- name: AUTH_LISTEN_ADDR
value: ":8080"
- name: AUTH_UPSTREAM_URL
value: "http://localhost:3000"
- name: AUTH_OIDC_AUTHORITY
value: "https://auth.example.com/realms/master"
- name: AUTH_OIDC_CLIENT_ID
value: "myapp"
- name: AUTH_OIDC_CALLBACK_PATH
value: "/auth/callback"
- name: AUTH_OIDC_SCOPES
value: "openid,profile,email"
- name: AUTH_OIDC_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: auth-oidc
key: cookie-secret
- name: AUTH_OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: auth-oidc
key: client-secret
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 50m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
MCP Mode:
name: authn
image: ghcr.io/fortedigital/auth-sidecar:latest
ports:
- containerPort: 8080
name: auth
protocol: TCP
env:
- name: AUTH_MODE
value: "mcp"
- name: AUTH_LISTEN_ADDR
value: ":8080"
- name: AUTH_LOG_LEVEL
value: "info"
- name: AUTH_UPSTREAM_URL
value: "http://localhost:3000"
- name: AUTH_MCP_RESOURCE
value: "https://mcp.example.com"
- name: AUTH_MCP_AUTHORIZATION_SERVERS
value: "https://auth.example.com"
- name: AUTH_MCP_SCOPES_SUPPORTED
value: "read,write"
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 50m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
Generated Resources¶
Secret (Token Mode):
apiVersion: v1
kind: Secret
metadata:
name: auth-tokens
namespace: <app-namespace>
labels:
app.kubernetes.io/managed-by: kyverno
app.kubernetes.io/created-by: inject-auth-sidecar
type: Opaque
data: {} # Populated by Helm chart
Secret (OIDC Mode):
apiVersion: v1
kind: Secret
metadata:
name: auth-oidc
namespace: <app-namespace>
labels:
app.kubernetes.io/managed-by: kyverno
app.kubernetes.io/created-by: inject-auth-sidecar
type: Opaque
data:
client-secret: <base64>
cookie-secret: <base64>
NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: <pod-name>-auth-ingress
namespace: <app-namespace>
labels:
app.kubernetes.io/managed-by: kyverno
app.kubernetes.io/created-by: inject-auth-sidecar
spec:
podSelector:
matchLabels: <pod-labels>
policyTypes:
- Ingress
ingress:
- ports:
- port: 8080
protocol: TCP
Excluded Namespaces¶
The policy does NOT apply to:
- kube-system
- kyverno
- argocd
- cert-manager
- monitoring
Health Checks¶
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
Request Flow¶
External Request → Traefik
↓
Service (port 8080)
↓
Pod: Auth Sidecar (port 8080)
├─ Validate credentials
│ • Token mode: Check Bearer token
│ • OIDC mode: Validate session or redirect to IdP
│ • MCP mode: OAuth 2.0 via RFC 9728 discovery / RFC 7591 dynamic registration
↓
Forward to Application (localhost:3000)
↓
Application processes request
See: Developer Guide - Enabling Authentication for usage examples.
Configuration Reference¶
Environment Variables¶
Common environment variables used across applications:
| Variable | Purpose | Example |
|---|---|---|
NODE_ENV |
Node.js environment | production |
PORT |
Application port | 3000 |
DB_HOST |
Database host | postgres |
DB_PORT |
Database port | 5432 |
DB_USER |
Database user | app_user |
DB_NAME |
Database name | app_db |
DB_PASSWORD |
Database password | From secret |
API_KEY |
External API key | From secret |
Resource Limits¶
Recommended resource allocation:
| Application Type | CPU Request | Memory Request | CPU Limit | Memory Limit |
|---|---|---|---|---|
| Lightweight API | 100m | 128Mi | 500m | 512Mi |
| Standard Web App | 200m | 256Mi | 1000m | 1Gi |
| Heavy Processing | 500m | 512Mi | 2000m | 2Gi |
| Database | 250m | 256Mi | 1000m | 1Gi |
Storage Classes¶
Default storage class used: UpCloud default (varies by provider)
API Endpoints¶
ArgoCD API¶
# Server
https://argocd.127.0.0.1.nip.io
# Applications endpoint
GET /api/v1/applications
# Application details
GET /api/v1/applications/{name}
# Sync application
POST /api/v1/applications/{name}/sync
Prometheus API¶
# Query endpoint
GET /api/v1/query?query={promql}
# Query range
GET /api/v1/query_range?query={promql}&start={time}&end={time}&step={duration}
# Metrics
GET /api/v1/label/__name__/values
Tempo API¶
# Search traces
GET /api/search?q={traceql}
# Get trace by ID
GET /api/traces/{traceID}
# Service tag values
GET /api/v2/search/tag/resource.service.name/values
Loki API¶
# Query logs
GET /loki/api/v1/query?query={logql}
# Query range
GET /loki/api/v1/query_range?query={logql}&start={time}&end={time}
# Push logs
POST /loki/api/v1/push
Glossary¶
Terms¶
App-of-Apps: ArgoCD pattern where a parent Application manages child Applications
GitOps: Operations approach where Git is the single source of truth
IngressRoute: Traefik CRD for routing external traffic to services
Multi-Source: ArgoCD feature allowing multiple Git sources per Application
SealedSecret: Encrypted secret that can be safely stored in Git
Sync Wave: Ordered deployment using annotations
Self-Heal: ArgoCD automatically reverts manual cluster changes
Prune: Automatically delete resources removed from Git
Annotations Reference¶
ArgoCD Annotations¶
# Sync wave (deployment order)
argocd.argoproj.io/sync-wave: "1"
# Refresh application
argocd.argoproj.io/refresh: "hard"
# Compare options
argocd.argoproj.io/compare-options: IgnoreExtraneous
# Sync options per resource
argocd.argoproj.io/sync-options: Prune=false
Kyverno Annotations¶
# Exclude from policy
policies.kyverno.io/exclude: "true"
# Severity
policies.kyverno.io/severity: high
Custom Annotations¶
# Authentication enabled
policies.forteapps.io/auth: "true"
# OIDC configuration
policies.forteapps.io/auth-oidc-authority: "https://..."
policies.forteapps.io/auth-oidc-client-id: "client-id"
Labels Reference¶
Standard Labels¶
# Application name
app.kubernetes.io/name: myapp
# Application instance
app.kubernetes.io/instance: myapp
# Application version
app.kubernetes.io/version: "1.0.0"
# Component type
app.kubernetes.io/component: frontend
# Part of larger application
app.kubernetes.io/part-of: ecommerce
# Managed by
app.kubernetes.io/managed-by: argocd
Custom Labels¶
# Allow secret cloning
allowedToBeCloned: "true"
# Environment
environment: production
# Team ownership
team: platform
Version Matrix¶
Component Versions¶
| Component | Version | Chart Version |
|---|---|---|
| ArgoCD | 2.9.0+ | Latest |
| Traefik | 2.10.0+ | Latest |
| Cert-Manager | 1.13.0+ | Latest |
| Kyverno | 1.10.0+ | Latest |
| Sealed Secrets | 0.24.0+ | Latest |
| Prometheus | 2.47.0+ | Latest |
| Grafana | 10.0.0+ | Latest |
| Loki | 2.9.0+ | Latest |
| Tempo | 2.6.0+ | 1.24.4 |
| Fluent-Bit | 2.1.0+ | Latest |
| Gitea | 1.25.4 | 12.5.0 |
| Gitea Act Runner | Latest | Latest |
| Renovate | v43.113.0 | 46.109.0 |
| PostgreSQL | 16-alpine | N/A |
| Trivy | Latest | Latest |
Kubernetes Compatibility¶
- Minimum: 1.24+
- Tested: 1.28+
- Recommended: Latest stable
Last Updated: 2026-04-16 Maintained By: Platform Team Version: 1.0.0