diff --git a/README.md b/README.md index 4178d57..f7ddeef 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This repository contains the complete GitOps configuration for our Kubernetes cl ### 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, Sealed Secrets, Homepage (platform dashboard) - **Business Applications**: MCP10X, MusicMan, Dot-AI Stack, ArgoCD MCP - **Policies**: Kyverno security policies for secret management, namespace controls, pod verification - **Monitoring**: Full observability stack with metrics, logs, traces, and alerting diff --git a/apps/overlays/upc-dev/dbunk-demo/dbunk-demo.yaml b/apps/overlays/upc-dev/dbunk-demo/dbunk-demo.yaml new file mode 100644 index 0000000..a2743c4 --- /dev/null +++ b/apps/overlays/upc-dev/dbunk-demo/dbunk-demo.yaml @@ -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 diff --git a/apps/overlays/upc-dev/dbunk-demo/kustomization.yaml b/apps/overlays/upc-dev/dbunk-demo/kustomization.yaml new file mode 100644 index 0000000..7bd806f --- /dev/null +++ b/apps/overlays/upc-dev/dbunk-demo/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- dbunk-demo.yaml diff --git a/apps/overlays/upc-dev/kustomization.yaml b/apps/overlays/upc-dev/kustomization.yaml index 1895aac..98e6a19 100644 --- a/apps/overlays/upc-dev/kustomization.yaml +++ b/apps/overlays/upc-dev/kustomization.yaml @@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../../base +- dbunk-demo # No patches needed — base already has "upc-dev" paths # upc-dev is the default/base cluster diff --git a/bootstrap.sh b/bootstrap.sh index 75af6fd..b2a9794 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -28,7 +28,6 @@ Bootstrap() Gitea() { echo "Installing secret..." - kubectl apply -f "secrets/" kubectl apply -f "private/${CLUSTER}/gitea-repo-main.yaml" kubectl apply -f "private/${CLUSTER}/main.key" } diff --git a/cluster-resources/policies/label-checker.yaml b/cluster-resources/policies/label-checker.yaml deleted file mode 100644 index 129007a..0000000 --- a/cluster-resources/policies/label-checker.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: kyverno.io/v1 -kind: ClusterPolicy -metadata: - name: require-labels - annotations: - policies.kyverno.io/title: Require Labels - policies.kyverno.io/category: Best Practices - policies.kyverno.io/minversion: 1.6.0 - policies.kyverno.io/severity: medium - policies.kyverno.io/subject: Pod, Label - policies.kyverno.io/description: Define and use labels that identify semantic attributes of your application or Deployment. A common set of labels allows tools to work collaboratively, describing objects in a common manner that all tools can understand. The recommended labels describe applications in a way that can be queried. This policy validates that the label `app.kubernetes.io/name` is specified with some value. -spec: - validationFailureAction: Audit - background: true - rules: - - name: check-for-labels - skipBackgroundRequests: true - exclude: - any: - - resources: - namespaces: - - kube-system - - istio-system - - argocd - - cert-manager - - monitoring - - secrets - - kyverno - match: - any: - - resources: - kinds: - - Pod - validate: - message: The label `app.kubernetes.io/name` is required. - allowExistingViolations: true - pattern: - metadata: - labels: - app.kubernetes.io/name: "?*" diff --git a/devbox.json b/devbox.json index ff78cb9..0f146ce 100644 --- a/devbox.json +++ b/devbox.json @@ -17,7 +17,9 @@ "claude-code@latest", "go@latest", "dotnet-sdk@latest", - "opentofu@1.11.6" + "opentofu@1.11.6", + "_1password@latest", + "github-cli@latest" ], "shell": { "init_hook": [ diff --git a/docs/DEVELOPER-GUIDE.md b/docs/DEVELOPER-GUIDE.md index 77e9465..739060d 100644 --- a/docs/DEVELOPER-GUIDE.md +++ b/docs/DEVELOPER-GUIDE.md @@ -1336,16 +1336,34 @@ stringData: | Field | Required | Description | |-------|----------|-------------| -| `clientId` | Yes | Keycloak client ID | -| `name` | Yes | Display name in Keycloak | -| `redirectUris` | Yes | Allowed redirect URIs | -| `webOrigins` | Yes | Allowed web origins (CORS) | -| `defaultClientScopes` | No | Scopes (default: `["openid", "email", "profile"]`) | -| `protocolMappers` | No | Custom claim mappers (default: `[]`) | -| `secret.namespace` | No | Namespace for the credential Secret (default: source namespace) | -| `secret.name` | No | Name of the credential Secret (default: `-oidc-credentials`) | -| `secret.keys.clientId` | No | Key name for client ID in credential Secret (default: `client-id`) | -| `secret.keys.clientSecret` | No | Key name for client secret in credential Secret (default: `client-secret`) | +| `clientId` | Yes | Keycloak client ID (must be unique in realm) | +| `name` | Yes | Display name in Keycloak UI | +| `redirectUris` | Yes | Allowed OAuth redirect URLs (supports wildcards like `/*`) | +| `webOrigins` | Yes | Allowed CORS origins | +| `defaultClientScopes` | No | OIDC scopes (default: `["openid", "email", "profile"]`) | +| `protocolMappers` | No | Custom claim mappers for tokens (see examples below) | +| `secret.namespace` | No | Target namespace for credentials (default: `source-namespace` annotation value) | +| `secret.name` | No | Credential Secret name (default: `-oidc-credentials`) | +| `secret.keys.clientId` | No | Key name for client ID (default: `client-id`) | +| `secret.keys.clientSecret` | No | Key name for client secret (default: `client-secret`) | + +**Protocol Mappers Example**: +```json +"protocolMappers": [ + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "config": { + "claim.name": "groups", + "full.path": "false", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } +] +``` #### Step 2: Reference the Credential Secret diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index efc0935..7bd9e3d 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -738,6 +738,59 @@ TLS terminates at Traefik; ArgoCD runs in insecure mode behind the proxy. ## 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 **Chart**: `traefik/traefik` @@ -1023,6 +1076,52 @@ dind: - Gitea admin panel (`/admin/runners`) — runners show as Online - Create test workflow in `.gitea/workflows/test.yml` — job executes +### Vaultwarden + +**Chart**: `guerzon/vaultwarden` +**Version**: 0.36.4 (app v1.36.0-alpine) +**Namespace**: `vaultwarden` + +**Purpose**: Self-hosted Bitwarden-compatible password manager. + +**Configuration**: +```yaml +# infra/overlays/upc-dev/vaultwarden/ + infra/values/ +domain: "https://bitwarden.forteapps.net" + +ingress: + enabled: true + class: "traefik" + tls: true + tlsSecret: vaultwarden-tls + hostname: bitwarden.forteapps.net + additionalAnnotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + +database: + type: postgresql + host: vaultwarden-postgresql # StatefulSet in overlay + existingSecret: prod-db-creds + +storage: + data: 5Gi (ReadWriteOnce) + attachments: 5Gi (ReadWriteOnce) +``` + +**TLS**: cert-manager auto-provisions Let's Encrypt certificate via `letsencrypt-prod` ClusterIssuer (same pattern as Gitea, Grafana, etc). + +**SSO**: Keycloak OIDC via `forte` realm (client ID: `vaultwarden`). Self-service client config Secret (`keycloak-client-vaultwarden`) triggers registrar to create KC client and sync credentials to `vaultwarden-oidc-credentials`. PKCE enabled. + +**Endpoints**: +- Web UI: `https://bitwarden.forteapps.net` + +**Database**: Separate ArgoCD Application `vaultwarden-postgresql` (sync-wave `"0"`) deploys PostgreSQL 16 StatefulSet + SealedSecret before Vaultwarden (wave `"1"`). 2Gi PVC. Chart does NOT include a PostgreSQL subchart — must be provisioned separately. + +**Secrets**: +- `prod-db-creds` (SealedSecret) — PostgreSQL credentials (`pgusername`, `pgpassword`) + SMTP credentials +- `vaultwarden-oidc-credentials` (registrar-managed) — OIDC client ID + secret +- `vaultwarden-tls` — auto-managed by cert-manager + ### AI Code Review (ai-review) **Type**: Gitea Actions workflow (`.gitea/workflows/ai-review.yaml`) @@ -1101,6 +1200,30 @@ ignore: - Check Gitea Actions tab for workflow run status and logs - Monitor Anthropic usage dashboard for token consumption +### Keycloak Browser Flow (IdP Auto-Redirect) + +**File**: `infra/values/base/keycloak-values.yaml` (inside `forte-realm.json`) + +The realm uses a custom browser authentication flow (`browser-auto-idp`) that skips the Keycloak login page and redirects directly to the Entra ID identity provider. + +**Flow executions**: + +| Priority | Authenticator | Requirement | Purpose | +|----------|--------------|-------------|---------| +| 10 | `auth-cookie` | ALTERNATIVE | Reuse existing session (no redirect) | +| 20 | `identity-provider-redirector` | ALTERNATIVE | Auto-redirect to `forte-entra` IdP | + +**Key fields in realm JSON**: +- `"browserFlow": "browser-auto-idp"` — overrides the default `browser` flow at realm level +- `"authenticationFlows"` — defines the custom flow with its executions +- `"authenticatorConfig"` — sets `defaultProvider: "forte-entra"` on the redirector + +**Why custom flow**: The default KC browser flow shows a username/password form with an IdP button. Since all authentication is via Entra ID, the custom flow eliminates this step. The `auth-cookie` execution preserves session reuse so returning users aren't redirected again. + +**Important**: The `forte-entra` identity provider must exist in Keycloak (currently configured manually in the KC admin console). If the IdP alias changes, update the `defaultProvider` value in the realm JSON. + +--- + ### Keycloak Client Registrar **Type**: CronJob (deployed via Keycloak Helm chart `extraDeploy`) @@ -1132,9 +1255,18 @@ ignore: **Resources**: - `ServiceAccount`: `keycloak-client-registrar` (namespace: `keycloak`) -- `ClusterRole`: `keycloak-client-registrar` (secrets: get/list/create/update/patch; namespaces: get/list) +- `ClusterRole`: `keycloak-client-registrar` + - Secrets: `get`, `list`, `create`, `update`, `patch` + - Namespaces: `get`, `list` - `ClusterRoleBinding`: `keycloak-client-registrar` - `CronJob`: `keycloak-client-registrar` + - **Schedule**: `*/2 * * * *` (every 2 minutes) + - **Concurrency Policy**: `Forbid` (prevents concurrent runs) + - **Backoff Limit**: 3 retries per job + - **History**: 1 successful job, 3 failed jobs retained + - **Resources**: 50m CPU / 64Mi memory (requests), 200m CPU / 128Mi memory (limits) + +**Container**: Alpine 3.20 with `curl` and `jq` installed **Kyverno Policy**: `keycloak-client-config-cloner` — clones labeled Secrets from app namespaces to `keycloak` namespace (see [Kyverno Policies](#kyverno-policies)) diff --git a/infra/base/homepage/homepage-extra-rbac.yaml b/infra/base/homepage/homepage-extra-rbac.yaml new file mode 100644 index 0000000..1549ab3 --- /dev/null +++ b/infra/base/homepage/homepage-extra-rbac.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: homepage-services-reader +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: homepage-services-reader +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: homepage-services-reader +subjects: +- kind: ServiceAccount + name: homepage + namespace: homepage diff --git a/infra/base/homepage/homepage-widget-credentials-sealed.yaml b/infra/base/homepage/homepage-widget-credentials-sealed.yaml new file mode 100644 index 0000000..3ab5e63 --- /dev/null +++ b/infra/base/homepage/homepage-widget-credentials-sealed.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: homepage-widget-credentials + namespace: homepage +spec: + encryptedData: + HOMEPAGE_VAR_GITEA_TOKEN: AgAVN1C931EQpn+sodr3CpjlhORfJVTW8aUr+pGZQb+65Pb8QLGeVGVa7Jv60gDJUX3r+93/jMrEbCOeDL6I4qCz/V35wMCxFZLnXIdkmto0W4MKt6cK8To1/OP7EhQJOGBlSuOFsrwoy+HDtvLIqmyF0nrxhTusm9/NHrw+gCVwSTPhiAX1MCuSOSRWpbXvyNphW8j7aqUaV6ixDt424Fe4alEIShYELcS3EX/VPgsf2p2bhvBRCQOh3LEprkuxSFMuPfCBk06TPTbIN4saNVm0Ke0zW/pxkVNSiIxEnKjOmpPJtacsfWN7du+nQbx276G2qvWrf+iawJVq0Z/SLikA/NUFBL6EjSRfgE3cSOri8sbxsd0AycsFGyp98EM29wE+WOQl52M/lwl02EmCivqkICSO7Jp9pM1ScbmRMa5vcnupsGbVDxhRKLqxhAskt/BXDkRzvHN31gH3YmelES3JuqNMHV0urFxmX2oOX9Pxbtv63csc+zhy1Ui5aoex7TPnLdk7kYLSAE2MSrzT6wHvVhBC5kNnDYVrLehvJrT+eNh0MOLx2wkuJmIOxRAGUyNi5DfDnP6qnvj2aefEymLuOXAIUXH8DbeBtrjsd74HX2hhIfBlPkXvhJR3ks7i5RXjK2/YYHkgJ+nJoW80S9N7ciaRy103g74TNJZt6QzzL5Vb80qZ6yQOD4G081KmTLDmhHjJVIIv9M3nLh2s0IeBV3/Z5qHZmtjN7sSaKAn4MIr5FaH9quhx + HOMEPAGE_VAR_GRAFANA_TOKEN: AgBloBlOlP+R/4VizE1CGpj0wyiwU14BemAnuUpld7OvOGc67dwfDPyponkQXjAZg3UU2cZ70A51WUAuVlAr+25Ktlf/FW2OBqj+1BJOCqMMyu+kv026yjX2aB8dKGzlTxgF8aji+j1mC8vP3vvmgI4Zf2HQAH7uFwLfeo8+QnV5EyhcExSS0xDne+VtOP9jNXbPRayry0DdyRVtaeKAiZacO+45oAJWszWOwmoMTg9FZQkLjER6Q0tyI6NnoNObsFCnh56chZTdzBOYtmPnwld1bP2FjoJDqn8AfRwbPTIj7t0eFP7WLUO7GQKpxVl+pFwJLb5xCOw2+HNtp1BhNCu7icuc0P88IlvwzkbN0lXJbYigVOzyjEo8f/al1DXPM4WaB/Nqmr7Mtt8KTRh2WMVTgiX5jsu25D0rGDvY9gqfBBqswkRhCLsG0v0EN32zXj1/52KYdmB7pk/+2lMwSaGMS11MOenHeU1Z95fGxm9f3EGF0E8xlFr4FowgsNwr+tJQqpM0bT/4mZnaQbGWtKPFizMtsfQFm+rHFcNCrGaOuecslmiIJs8lTm18KlrncsGfxNS64tVXk+LvydU0rwybvpg2rQjEWtAl1IQsaaiz96OAlYxxK1MGxN7KE6F8R4kfnWTZ5Fs1KMmd/DOIVBXyCbqXxk8pbekmaIeNSfv92JNZ0QNJWsBa2vgQ24WI2pb4XiR0BvtLpt3BVlZUcSK92SzUblWmYWVMwHYCJkEeEUV1PhYEmyiN+V/Kq5Qb + template: + metadata: + creationTimestamp: null + name: homepage-widget-credentials + namespace: homepage diff --git a/infra/base/homepage/homepage.yaml b/infra/base/homepage/homepage.yaml new file mode 100644 index 0000000..5caf146 --- /dev/null +++ b/infra/base/homepage/homepage.yaml @@ -0,0 +1,43 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: homepage + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "3" + labels: + app.kubernetes.io/name: homepage + app.kubernetes.io/part-of: platform + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: https://jameswynn.github.io/helm-charts + chart: homepage + targetRevision: "2.1.0" + helm: + releaseName: homepage + valueFiles: + - $values/infra/values/base/homepage-values.yaml + - $values/infra/values/upc-dev/homepage-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: homepage + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true diff --git a/infra/base/homepage/kustomization.yaml b/infra/base/homepage/kustomization.yaml new file mode 100644 index 0000000..d2c23da --- /dev/null +++ b/infra/base/homepage/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- homepage.yaml +- homepage-widget-credentials-sealed.yaml +- homepage-extra-rbac.yaml diff --git a/infra/base/keycloak/keycloak.yaml b/infra/base/keycloak/keycloak.yaml index 937a243..96ebab5 100644 --- a/infra/base/keycloak/keycloak.yaml +++ b/infra/base/keycloak/keycloak.yaml @@ -43,10 +43,6 @@ spec: - ServerSideApply=true ignoreDifferences: - - group: batch - kind: CronJob - jsonPointers: - - /spec/jobTemplate/spec/template/spec/containers/0/args - group: apps kind: StatefulSet jsonPointers: diff --git a/infra/base/kustomization.yaml b/infra/base/kustomization.yaml index 1f216f1..6e802ef 100644 --- a/infra/base/kustomization.yaml +++ b/infra/base/kustomization.yaml @@ -21,3 +21,5 @@ resources: - grafana-dashboards - karpor - databunker +- homepage +- vault diff --git a/infra/base/kyverno-policies/kyverno-policies.yaml b/infra/base/kyverno-policies/kyverno-policies.yaml index e00e063..ab0a25f 100644 --- a/infra/base/kyverno-policies/kyverno-policies.yaml +++ b/infra/base/kyverno-policies/kyverno-policies.yaml @@ -27,7 +27,6 @@ spec: automated: prune: true selfHeal: true - allowEmpty: false syncOptions: - CreateNamespace=true - Validate=true diff --git a/infra/base/vault/kustomization.yaml b/infra/base/vault/kustomization.yaml new file mode 100644 index 0000000..9d00240 --- /dev/null +++ b/infra/base/vault/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- vault.yaml diff --git a/infra/base/vault/vault.yaml b/infra/base/vault/vault.yaml new file mode 100644 index 0000000..dfeea4e --- /dev/null +++ b/infra/base/vault/vault.yaml @@ -0,0 +1,49 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + labels: + app.kubernetes.io/name: vault + app.kubernetes.io/part-of: security + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: https://helm.releases.hashicorp.com + chart: vault + targetRevision: "0.32.0" + helm: + releaseName: vault + valueFiles: + - $values/infra/values/base/vault-values.yaml + - $values/infra/values/upc-dev/vault-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: vault + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/infra/overlays/aks-dev/kustomization.yaml b/infra/overlays/aks-dev/kustomization.yaml index 60edc78..9e7fa41 100644 --- a/infra/overlays/aks-dev/kustomization.yaml +++ b/infra/overlays/aks-dev/kustomization.yaml @@ -13,9 +13,19 @@ resources: - ../../base/prometheus - ../../base/sealedsecrets - ../../base/tempo +- ../../base/homepage - ../../base/traefik-application patches: +# Homepage: swap upc-dev → aks-dev +- target: + kind: Application + name: homepage + patch: | + - op: replace + path: /spec/sources/0/helm/valueFiles/1 + value: $values/infra/values/aks-dev/homepage-values.yaml + # Traefik: swap upc-dev → aks-dev - target: kind: Application diff --git a/infra/overlays/upc-dev/kustomization.yaml b/infra/overlays/upc-dev/kustomization.yaml index be1f13c..fac7510 100644 --- a/infra/overlays/upc-dev/kustomization.yaml +++ b/infra/overlays/upc-dev/kustomization.yaml @@ -2,6 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ../../base +- vaultwarden-postgresql +- vaultwarden # No patches needed — base already has "upc-dev" paths # upc-dev is the default/base cluster diff --git a/infra/overlays/upc-dev/vaultwarden-postgresql/kustomization.yaml b/infra/overlays/upc-dev/vaultwarden-postgresql/kustomization.yaml new file mode 100644 index 0000000..e3e2778 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden-postgresql/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- vaultwarden-postgresql.yaml diff --git a/infra/overlays/upc-dev/vaultwarden-postgresql/resources/kustomization.yaml b/infra/overlays/upc-dev/vaultwarden-postgresql/resources/kustomization.yaml new file mode 100644 index 0000000..b02c8e8 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden-postgresql/resources/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- postgresql.yaml +- vaultwarden-db-secret-sealed.yaml diff --git a/infra/overlays/upc-dev/vaultwarden-postgresql/resources/postgresql.yaml b/infra/overlays/upc-dev/vaultwarden-postgresql/resources/postgresql.yaml new file mode 100644 index 0000000..f638543 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden-postgresql/resources/postgresql.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +kind: Service +metadata: + name: vaultwarden-postgresql + namespace: vaultwarden + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: vaultwarden + app.kubernetes.io/component: database +spec: + type: ClusterIP + ports: + - name: tcp-postgresql + port: 5432 + targetPort: tcp-postgresql + selector: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: vaultwarden +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: vaultwarden-postgresql + namespace: vaultwarden + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: vaultwarden + app.kubernetes.io/component: database +spec: + serviceName: vaultwarden-postgresql + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: vaultwarden + template: + metadata: + labels: + app.kubernetes.io/name: postgresql + app.kubernetes.io/instance: vaultwarden + app.kubernetes.io/component: database + spec: + containers: + - name: postgresql + image: postgres:16-alpine + ports: + - name: tcp-postgresql + containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: prod-db-creds + key: pgusername + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: prod-db-creds + key: pgpassword + - name: POSTGRES_DB + value: vaultwarden + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + livenessProbe: + exec: + command: + - sh + - -c + - pg_isready -U "$POSTGRES_USER" -d vaultwarden + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - sh + - -c + - pg_isready -U "$POSTGRES_USER" -d vaultwarden + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/infra/overlays/upc-dev/vaultwarden-postgresql/resources/vaultwarden-db-secret-sealed.yaml b/infra/overlays/upc-dev/vaultwarden-postgresql/resources/vaultwarden-db-secret-sealed.yaml new file mode 100644 index 0000000..3592873 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden-postgresql/resources/vaultwarden-db-secret-sealed.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: prod-db-creds + namespace: vaultwarden +spec: + encryptedData: + DATABASE_URL: AgAy1d//kBevUqZxB5KAXd+qxm8QykNxp5J1QP7Y30XCpE8hldPXF+NIx3w0B0PUyMAVsa+JrBmCMtzgibddIJqqF2upu/8EYxJZNusrJPOi47g3VcZIg1WyxoFfffH6jwhzv69TE/T8WGBXmWbD2vr+XWjE24Q+lgwLut0mocVfihUjpuYq0WDjgJx7pqLnY1VatTwgSkAv1uRRVqdi7e1M5isDNEpdItCbEoWwdvZhG5JIMAA/2vecY4/vEne3cg46lJAkv4ueZNATG6DOXGgQgz6h7zCKSGS1xTfGr+4A2V2/vSYpQ/r8Td37mlseoBvwN4H5O+FgHrVREm7N6aafDariYd+ZfqUIGObsZIXhxhDmAM96pjtP8ehYVwq1srWTU+SUewEmwLFWhVP1UFnTB5vgIuOWoKjHlS7dSUpStpw2u7/mQ6vhRhEaDqY6cNzJgM9hipQM/pt5an7z4ovWVeAeK8InGzKU+uxOpv/oxmi9N54B+5O4DVZC+BIbFXchxDvqivRcZrTK+CNjHjkk4We5MvN0qlhSuCYOGzEVaQ192yHciDoncw58D7fG4NicT2AcCJDVIwRGG05wqKal7g61g7Qg16oqdZIauKIU7ChSgBk7Xv33biZ4ZPe+JoSEmp9izJ8R7yyNO3KJgqH7iQ2UQzXDUqhfTr6w/oIdFST8sQtEIo7o1Z5JoXpzd4R1gVkcPWqtbbB22iRrDJsofeW+yvgGqyZLsWT2bKuDMLUn3GiqvaHaeQ9AbhNBnl6Qth3X8heBm2Zle1xxapFktisPwBQC7FDlukgvkiAOO7BywVI0+ITU4KLLOAftVqHmZ4fgDDqNFg8= + SMTP_PASSWORD: AgCbhM99+DbSTCcuxtcccOrZq99GjR51B5SkW6DNxz1jW2q4lgc6o21+gHEGGAamRDg/mWyu2UBBNgEzrW024558qP6SjVPwJclWhNZHyL4R4MCA0kv/kqNa43na2j3pq7kzT0MrqPAXD8IJrtYPxr+ngiuo7MCRef/TzEfwbwK+KgkeB1g+9ErwHWr5tU1B+3yAn5tfNeHmGFrLX230mra2ie+ntNx9kn80mb5TYoJ/mSaX7wv4bqZGEHkg7isXrMoMROXLBepCvP0zi3ta+SKJjHy73TvhKSzo7/ZHxapHw3oaazdpKJobeolWB7SziWsbXfT0gYYim3LrcieQeb7hhN3OAvSYx1Z/3UefEE1o3GFEzK1UV9ISwU/mBjMDZWyM7dK9lSbuPGvbpKU1jjP8qZWQP5v2SIT6Nb5TQgNeATdO3RkjoviqRzjJfpuu+oe5paXNeEIUSTskL7HuUjWj13DX1zDqQWTR1/Tb9ntxRtgIoAl2ymDsOednurnJeX71OJ3G++Oc8b5P2EkNQuEwaYb9raNHqZkCXeRpQVyEGIUh9sAGZUplUJSues/Jw25WuFoNBfMi/F022Mkvfnp/s0J+QrMIxSRvqiWS+7hODknvE3Zf2DI+DKClWxspfv084DAkOSV1Lq9fqNy/bKoJR/5MVHmxbW6teza6zXBnwn2nk+2DZ8IU20P15aVdtKR2zG9u8hnrqWrTQHTdvzb34WKjC0km4/MaIGKvboyw + SMTP_USERNAME: AgCijAuSvlGeMME1FyPXQdOTGKktyXfT8tjBK/ozt3X8O5hebFVvM61hNLDQTe9V/O/NLw6P1wbV4V+wtztqLs9TPDyq9L3D9JUEDjh97gWxVf7nFf8/d24F0PJoR8srKp3onb7+Rx02318RUp+gp7YsXbK6YBfkaOsoD08UgrFY3ARnP4eIirRICNCdouPgKkh038yAWcQDA20GBN2faJlQSejS4cr8WAa/ME1fRe40Q4tewgiX7hK5BWjt2eoovLrPzcHvbgTbfjwUWvmeiSicnF+VLrc3M56aKML2xuAggBk4vKtla/Af1pJfjv+Ut2z6VzECfIth12yQa6kIvKOE8aa3o/6oKLRJQRO8rQOOmLiInR+jQxrOmYw+OzkON07nBIeBon7nKYSIMiBt68kXKWJ9ZDexF7WGOHsFgUo4hA/z/c/Ccg6XHgxFt7uXZYLyziihQXy33zIsvu0rXOaczT5j3NJIhO60jz2Q8ig9JuWhd7+Dnd9EFMC0WJOM9kjpxeSjg4MYjH7VGnnEDF2GAQTlwDn5HwZJXRQFHfdCiHS5CCpfmak9MtIC9uoIsbPAnEhdTC20wDvIGlR5O4JGlQRyirKKmCdT1MJt8zi5d+EVQiicVZLGGD6nsqCNchvFYcx+glMYqWWsmyS6M+uVaYa/Fb/n+0QR6GNCF3Qi7voMJf2bFxFDk78fk76MG919XPQVAG/FPliNyWJJ7gEM+j/rDYuSrul14duV + adminToken: AgCVSgRK39WksTX6GogBvMZtPMd4XzMFkKSPhjTCm8pllzuc88pS2LXHxyGDWVLf+GZXRTCNrf5JLPst8/GTvOX17J9scRAjmhoyVVgzFNbcqGI/czLf9vH6JnhorMmUHxS+kWBm3oLZjhW3f/9vq0xzSbut87VDsyenaK4EcFLrYnfVRlMrwxL9oxPPTeB+Wah/uGoyLNfc7MMLK+/sUma4RO61VJ8YvmLUnwYNFosUcbF2B9hXMSzKKCJ+ihXYIdNIz2YWdMmj7a3D+Mor5x4jIjC+tunT4Zge5L1mx0i92KPgPkP48jKxayIZ/FxnYej+ggU6PmdYD1B6wgUbgLQV6ccwysjHce7ICvGWiM+PozFWI1qEVKqtNgp3kwInThD63Hyub9Euj0g/SE/nZcwy8DuIEhsLpLSdvxDT2lCps0VdXcb01kuta/3i4vgozSBvdIL1FrQKPLqF5+6ZEQ6BdZDkgxVqQ845WZoGbfqIyI03BMvsx/8HB5c+0Y0w0C6DRpEW55T9yRTZqzvqWUOpXy5/L2Nc9G4PCUK3lY0QPIFlPCBDl+cKgfP/XvKhMFcFpwp/Ssd0o+RhgOMBHJ5qk7RxAN1k2Y6Tkros39eUKWbM6UQ97LjrAdLcgJKX86ZkEChdeeKOnSxRmqHFXx5m4Wa6syixjipeQPwybCYQA9TgNBv0lToA7gi+lcVvXkoAgYIeB9Cp0j8UVEwdgvwWp8YgkpuWhPCAafhboH8qGhHhVfKADmlB05vUItZ0eoEVv7D9SqUTFueUmoZXwo67uyMLZhQo7eGr9+k8hiesCoEm11HCTa4DvF9zE54zgkyt0TMsKAEKmX47zJwnw76zidZC/K9e + pgpassword: AgCeQLwzajVBHW+o1ZSAcfbcJXny5K7UxxMfjud0j3hm9qx3TlLaFJrNoWd4XCuHAW1Ybl23ynWm+8kpXfZv95bXGkl0jZIm989MlAJQYKcaSTdhIM6Bkl3Cbr8kTVj2yrylGPJ96QISKNoiV7IetU82t8f21kxjyrNwt4lQEwWRFjW45jozb32qFXRkTioJ1WKaeJkyRSHWn7nLVitqZ4QjZzoN9AC7cUDOY/FffEiz7QRYSf6xaVPqQfCeH6QVtfmwtU5qNTcthOW0+/LQKpkg8i2nuoAsyOe7tsOYCENbDGkLckNTns2oPvDnH5eA8F5VDlnyJtROlwKjWfTC2zjazZynpA6cBH9XzX3Pg9P45G5HJ/4TVSUbTMCSVrbW8Ec+zdA2P4l3ytfVM2ER4N2/bQrUEVW2/ngDzhsKllmOPEN/M7b/VTed7U1xyJHsoT+AJEqVJO/vFBRxpI/ZacD+m5kWf3sp1SUWSgkUi+YWH8xuRurM2/kiUGwwUXu5ZMumjpMTkJBfX4NYRezq0DCsgaXdO2yI4xTfAysj78UikdU0PKyAXPcT8y55kJQ1jECszFauyeSYh0FpLu/XAmeB8dunlBqgISLszwnnman0/ThLeo96MFo6WPXoAeMTy8+GfTSH5tkmm8TVD7A+gRHprfFS66eY5Zi38OPU+VGRJS1J8yLSD2cNvCFR/sz1Br9VV7IySx1S+HvbEd7BwmdWrIoEcVUwIAD/U0Uxbw4sr7wLQ2wFQjDil4VItA== + pgusername: AgAcgel8GpdRVThE9i4EfFwcJYHEPnCyi4jHEaAltn4VMtub6hvhtH+ihxS97AhK6kgrTLEgtlfb07n1EZ3AK18A+ci9P5ulP0GgwMrMt1yxiHliqInvZPNb8vWC/2f2lZm7EFrBvYqMFRPV10YygtkW3Kku8h31l0xc2/DUKqZ2hMymIjx7hupabUOhnPJeF+IxVhTcsAa6SjhppX1ED0R/Im73pxTxbfLQETkHd5KcB4h6RLIPvuNiG5uftkPP8qjJqFIhNew+0IxMrsTzJbyLx90vqI6LHHtsrNDAsOHqWGnyZqIdkK+Bagm84VlpBfIdz8Fs0YqzBQaZts4+QXByzkcQZBDy11Hg9FCjdUVk3VgVLDQj+hL+aWTeJ0Iui4zW4LMSB0Dk0ZQeQa4BtwuDllky5GUA7sLmkbceAWymBNzNXSOeeAi1qfR6h6rNnh0x5yOnSN8soReNaCEYBSa8HjUamvuzu4yn+fKOJyI50QEJ7hKLXLaqyiNpa5bJw4FQvGM7qESkZW2BZLEvkq4mlKjzvSm891DozA44Zy1s/3CvytFsUtaWmquSvqqHWUxqEEemJ8zOCFXEYo1TjEfkYIUipM5KJ+PrpQGTgjdnL8FbAv3r3NG7DoucQtVeJJVeaBqOZcfEQw4Xz15grsdjggFunhmz+ZjWjgEwV4G/dCmeus3SBWJWLMOoPWCvBWrQZRQq9KVj + template: + metadata: + creationTimestamp: null + name: prod-db-creds + namespace: vaultwarden diff --git a/infra/overlays/upc-dev/vaultwarden-postgresql/vaultwarden-postgresql.yaml b/infra/overlays/upc-dev/vaultwarden-postgresql/vaultwarden-postgresql.yaml new file mode 100644 index 0000000..8a0d1d2 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden-postgresql/vaultwarden-postgresql.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vaultwarden +--- + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vaultwarden-postgresql + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "0" + labels: + app.kubernetes.io/name: vaultwarden-postgresql + app.kubernetes.io/part-of: security + app.kubernetes.io/managed-by: argocd + 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/upc-dev/vaultwarden-postgresql/resources + + destination: + server: https://kubernetes.default.svc + namespace: vaultwarden + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true + + ignoreDifferences: + - group: apps + kind: StatefulSet + jsonPointers: + - /spec/volumeClaimTemplates diff --git a/infra/overlays/upc-dev/vaultwarden/keycloak-client-config.yaml b/infra/overlays/upc-dev/vaultwarden/keycloak-client-config.yaml new file mode 100644 index 0000000..552f281 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden/keycloak-client-config.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-client-vaultwarden + namespace: vaultwarden + labels: + keycloak.forteapps.net/client-config: "true" +stringData: + client.json: | + { + "clientId": "vaultwarden", + "name": "Vaultwarden", + "redirectUris": ["https://vaultwarden.forteapps.net/*"], + "webOrigins": ["https://vaultwarden.forteapps.net"], + "protocolMappers": [], + "secret": { + "namespace": "vaultwarden", + "name": "vaultwarden-oidc-credentials", + "keys": { "clientId": "client-id", "clientSecret": "client-secret" } + } + } diff --git a/infra/overlays/upc-dev/vaultwarden/kustomization.yaml b/infra/overlays/upc-dev/vaultwarden/kustomization.yaml new file mode 100644 index 0000000..46b4f10 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- vaultwarden.yaml +- keycloak-client-config.yaml diff --git a/infra/overlays/upc-dev/vaultwarden/vaultwarden.yaml b/infra/overlays/upc-dev/vaultwarden/vaultwarden.yaml new file mode 100644 index 0000000..1d41fd8 --- /dev/null +++ b/infra/overlays/upc-dev/vaultwarden/vaultwarden.yaml @@ -0,0 +1,43 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vaultwarden + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + labels: + app.kubernetes.io/name: vaultwarden + app.kubernetes.io/part-of: security + app.kubernetes.io/managed-by: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + sources: + - repoURL: https://guerzon.github.io/vaultwarden + chart: vaultwarden + targetRevision: "0.36.4" + helm: + releaseName: vaultwarden + valueFiles: + - $values/infra/values/base/vaultwarden-values.yaml + - $values/infra/values/upc-dev/vaultwarden-values.yaml + + - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git + targetRevision: HEAD + ref: values + + destination: + server: https://kubernetes.default.svc + namespace: vaultwarden + + syncPolicy: + automated: + prune: true + selfHeal: true + allowEmpty: false + syncOptions: + - CreateNamespace=true + - Validate=true + - ServerSideApply=true diff --git a/infra/values/aks-dev/homepage-values.yaml b/infra/values/aks-dev/homepage-values.yaml new file mode 100644 index 0000000..101157e --- /dev/null +++ b/infra/values/aks-dev/homepage-values.yaml @@ -0,0 +1,15 @@ +ingress: + main: + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: start.forteapps.net + paths: + - path: / + pathType: Prefix + tls: + - secretName: homepage-tls + hosts: + - start.forteapps.net diff --git a/infra/values/base/argocd-values.yaml b/infra/values/base/argocd-values.yaml index 662812b..ddd6962 100644 --- a/infra/values/base/argocd-values.yaml +++ b/infra/values/base/argocd-values.yaml @@ -35,6 +35,12 @@ server: ingressClassName: traefik annotations: cert-manager.io/cluster-issuer: letsencrypt-prod + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "ArgoCD" + gethomepage.dev/description: "GitOps continuous delivery" + gethomepage.dev/group: "DevOps" + gethomepage.dev/icon: "argo-cd" + gethomepage.dev/href: "https://argocd.forteapps.net" tls: true extraArgs: - --insecure diff --git a/infra/values/base/gitea-values.yaml b/infra/values/base/gitea-values.yaml index 619f779..2bc7fbb 100644 --- a/infra/values/base/gitea-values.yaml +++ b/infra/values/base/gitea-values.yaml @@ -41,6 +41,7 @@ gitea: oauth2: ENABLED: true ENABLE_AUTO_REGISTRATION: true + ACCOUNT_LINKING: auto USERNAME: email session: @@ -114,6 +115,15 @@ ingress: className: traefik annotations: cert-manager.io/cluster-issuer: letsencrypt-prod + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Gitea" + gethomepage.dev/description: "Git hosting & CI/CD" + gethomepage.dev/group: "DevOps" + gethomepage.dev/icon: "gitea" + gethomepage.dev/href: "https://git.forteapps.net" + gethomepage.dev/widget.type: "gitea" + gethomepage.dev/widget.url: "https://git.forteapps.net" + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_GITEA_TOKEN}}" hosts: - host: git.forteapps.net paths: diff --git a/infra/values/base/grafana-values.yaml b/infra/values/base/grafana-values.yaml index 81e2dbf..9d229f5 100644 --- a/infra/values/base/grafana-values.yaml +++ b/infra/values/base/grafana-values.yaml @@ -3,11 +3,21 @@ ingress: ingressClassName: traefik annotations: cert-manager.io/cluster-issuer: letsencrypt-prod + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Grafana" + gethomepage.dev/description: "Metrics & observability dashboards" + gethomepage.dev/group: "Monitoring" + gethomepage.dev/icon: "grafana" + gethomepage.dev/href: "https://grafana.forteapps.net" tls: - secretName: grafana-tls hosts: - grafana.forteapps.net +persistence: + enabled: true + size: 1Gi + resources: requests: cpu: 50m diff --git a/infra/values/base/homepage-values.yaml b/infra/values/base/homepage-values.yaml new file mode 100644 index 0000000..4ab2dce --- /dev/null +++ b/infra/values/base/homepage-values.yaml @@ -0,0 +1,72 @@ +# Homepage Helm Values +# Chart: jameswynn/homepage — https://gethomepage.dev +# Discovery: K8s service annotations (gethomepage.dev/*) +# Each deployed app annotates its own Service — apps not deployed = not visible. + +# RBAC ClusterRole — required for cluster-wide service annotation scanning +enableRbac: true + +serviceAccount: + create: true + name: homepage + +config: + # Scan all namespaces for services with gethomepage.dev/enabled: "true" + kubernetes: + mode: cluster + traefik: true + + settings: + title: "Platform" + headerStyle: clean + layout: + Apps: + style: row + columns: 3 + Security: + style: row + columns: 3 + Tools: + style: row + header: false + columns: 2 + DevOps: + style: column + rows: 2 + Monitoring: + style: column + rows: 1 + + # Top-of-page cluster overview widget + widgets: + - kubernetes: + cluster: + show: true + cpu: true + memory: true + showLabel: true + label: "Cluster" + nodes: + show: true + cpu: true + memory: true + showLabel: true + # In-cluster entries come from K8s service annotations. + # External (out-of-cluster) services are listed here statically. + bookmarks: [] + services: [] + +resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: 100m + memory: 256Mi + +env: +- name: HOMEPAGE_ALLOWED_HOSTS + value: start.forteapps.net +envFrom: +- secretRef: + name: homepage-widget-credentials diff --git a/infra/values/base/keycloak-values.yaml b/infra/values/base/keycloak-values.yaml index 109d14d..c305055 100644 --- a/infra/values/base/keycloak-values.yaml +++ b/infra/values/base/keycloak-values.yaml @@ -18,6 +18,12 @@ ingress: ingressClassName: traefik annotations: cert-manager.io/cluster-issuer: letsencrypt-prod + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Keycloak" + gethomepage.dev/description: "Identity & access management" + gethomepage.dev/group: "Security" + gethomepage.dev/icon: "keycloak" + gethomepage.dev/href: "https://id.forteapps.net/admin/forte-test/console/" metrics: enabled: true @@ -52,6 +58,9 @@ keycloakConfigCli: enabled: true image: repository: bitnamilegacy/keycloak-config-cli + extraEnvVars: + - name: IMPORT_MANAGED_PROTOCOL_MAPPER + value: "no-delete" configuration: forte-realm.json: | { @@ -95,6 +104,18 @@ keycloakConfigCli: "access.token.claim": "true", "userinfo.token.claim": "true" } + }, + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "config": { + "claim.name": "groups", + "full.path": "false", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } } ] }, @@ -167,7 +188,54 @@ keycloakConfigCli: ] } ], + "browserFlow": "browser-auto-idp", + "authenticationFlows": [ + { + "alias": "browser-auto-idp", + "description": "Browser flow with auto-redirect to Forte Entra IdP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10 + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "authenticatorConfig": "forte-entra-redirector" + } + ] + } + ], + "authenticatorConfig": [ + { + "alias": "forte-entra-redirector", + "config": { + "defaultProvider": "forte-entra" + } + } + ], "groups": [ + { + "name": "k8s", + "path": "/k8s", + "clientRoles": { + "grafana": ["Editor"] + } + }, + { + "name": "dev", + "path": "/dev", + "clientRoles": { + "grafana": ["Viewer"] + } + }, { "name": "ArgoCD Admins", "path": "/ArgoCD Admins" @@ -437,10 +505,10 @@ extraDeploy: CRED_SECRET_KEY=$(echo "$CLIENT_JSON" | jq -r '.secret.keys.clientSecret // "client-secret"') # Check if credential Secret already exists in target namespace - CRED_EXISTS=$(curl -sf -o /dev/null -w "%{http_code}" \ + CRED_EXISTS=$(curl -s -o /dev/null -w "%{http_code}" \ --cacert "$CA_CERT" \ -H "Authorization: Bearer ${SA_TOKEN}" \ - "${K8S_API}/api/v1/namespaces/${CRED_NS}/secrets/${CRED_NAME}") + "${K8S_API}/api/v1/namespaces/${CRED_NS}/secrets/${CRED_NAME}" || echo "000") # Skip if hash matches and credential Secret exists if [ "$CONFIG_HASH" = "$EXISTING_HASH" ] && [ "$CRED_EXISTS" = "200" ]; then @@ -460,44 +528,47 @@ extraDeploy: publicClient: false, redirectUris: .redirectUris, webOrigins: .webOrigins, - defaultClientScopes: .defaultClientScopes, protocolMappers: (.protocolMappers // []) - }') + } | with_entries(select(.value != null))') # Check if client already exists - EXISTING=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ - "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" \ - | jq -r '.[0].id // empty') + EXISTING_RESPONSE=$(curl -s -H "Authorization: Bearer ${TOKEN}" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" || true) + EXISTING=$(echo "$EXISTING_RESPONSE" | jq -r '.[0].id // empty' 2>/dev/null || true) if [ -n "$EXISTING" ]; then echo " Updating existing Keycloak client (uuid: ${EXISTING})" - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + RESPONSE=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -X PUT -d "$KC_CLIENT" \ - "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${EXISTING}") + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${EXISTING}" || true) + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d') if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "200" ]; then - echo " ERROR: Failed to update client '${CLIENT_ID}' (HTTP ${HTTP_CODE})" + echo " ERROR: Failed to update client '${CLIENT_ID}' (HTTP ${HTTP_CODE}): ${RESPONSE_BODY}" annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/sync-status" "error" continue fi CLIENT_UUID="$EXISTING" else echo " Creating new Keycloak client '${CLIENT_ID}'" - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + RESPONSE=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -X POST -d "$KC_CLIENT" \ - "${KEYCLOAK_URL}/admin/realms/${REALM}/clients") + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients" || true) + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d') if [ "$HTTP_CODE" != "201" ]; then - echo " ERROR: Failed to create client '${CLIENT_ID}' (HTTP ${HTTP_CODE})" + echo " ERROR: Failed to create client '${CLIENT_ID}' (HTTP ${HTTP_CODE}): ${RESPONSE_BODY}" annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/sync-status" "error" continue fi # Fetch the newly created client's UUID - CLIENT_UUID=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ + CLIENT_UUID=$(curl -s -H "Authorization: Bearer ${TOKEN}" \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" \ - | jq -r '.[0].id') + | jq -r '.[0].id' || true) fi # Sync credentials to target namespace diff --git a/infra/values/base/vault-values.yaml b/infra/values/base/vault-values.yaml new file mode 100644 index 0000000..0141ef2 --- /dev/null +++ b/infra/values/base/vault-values.yaml @@ -0,0 +1,36 @@ +# HashiCorp Vault Helm Chart Values +# Chart: hashicorp/vault v0.32.0 + +server: + standalone: + enabled: true + + dataStorage: + enabled: true + size: 5Gi + + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + + ingress: + enabled: true + ingressClassName: traefik + pathType: Prefix + activeService: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Vault" + gethomepage.dev/description: "Secrets management" + gethomepage.dev/group: "Security" + gethomepage.dev/icon: "vault" + gethomepage.dev/href: "https://vault.forteapps.net" + +ui: + enabled: true + serviceType: ClusterIP diff --git a/infra/values/base/vaultwarden-values.yaml b/infra/values/base/vaultwarden-values.yaml new file mode 100644 index 0000000..d7bbc71 --- /dev/null +++ b/infra/values/base/vaultwarden-values.yaml @@ -0,0 +1,3 @@ +image: + tag: "1.36.0-alpine" +domain: "https://vaultwarden.forteapps.net" diff --git a/infra/values/upc-dev/databunker-values.yaml b/infra/values/upc-dev/databunker-values.yaml index ab60a39..c126a70 100644 --- a/infra/values/upc-dev/databunker-values.yaml +++ b/infra/values/upc-dev/databunker-values.yaml @@ -1,3 +1,10 @@ ingress: enabled: true host: databunker.forteapps.net + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Databunker" + gethomepage.dev/description: "Secure Database for PII and PCI Records" + gethomepage.dev/group: "Security" + gethomepage.dev/icon: "double-take" + gethomepage.dev/href: "https://databunker.forteapps.net" diff --git a/infra/values/upc-dev/homepage-values.yaml b/infra/values/upc-dev/homepage-values.yaml new file mode 100644 index 0000000..ac71704 --- /dev/null +++ b/infra/values/upc-dev/homepage-values.yaml @@ -0,0 +1,69 @@ +ingress: + main: + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: start.forteapps.net + paths: + - path: / + pathType: Prefix + tls: + - secretName: homepage-tls + hosts: + - start.forteapps.net + +config: + settings: + title: "Forte Platform" + headerStyle: clean + layout: + Apps: + style: row + columns: 2 + Security: + style: row + columns: 3 + Tools: + style: row + header: false + columns: 2 + DevOps: + style: column + rows: 2 + Monitoring: + style: column + rows: 1 + + # Top-of-page cluster overview widget + widgets: + - kubernetes: + cluster: + show: true + cpu: true + memory: true + showLabel: true + label: "Cluster" + nodes: + show: true + cpu: true + memory: true + showLabel: true + # In-cluster entries come from K8s service annotations. + # External (out-of-cluster) services are listed here statically. + bookmarks: [] + services: + - Apps: + - Forte Benken: + href: https://benken.hackathon.forteapps.net + description: Teknisk kompetanse fra offentlige anbud + icon: forte + - Forte Drop: + href: https://drop.hackathon.forteapps.net + description: Self-hosted HTML-drops + MCP for Claude + icon: forte + - Forte Feedback: + href: https://feedback.forteapps.net + description: Fortes internal feedback app + icon: forte diff --git a/infra/values/upc-dev/vault-values.yaml b/infra/values/upc-dev/vault-values.yaml new file mode 100644 index 0000000..f6755f9 --- /dev/null +++ b/infra/values/upc-dev/vault-values.yaml @@ -0,0 +1,9 @@ +server: + ingress: + hosts: + - host: vault.forteapps.net + paths: [] + tls: + - secretName: vault-tls + hosts: + - vault.forteapps.net diff --git a/infra/values/upc-dev/vaultwarden-values.yaml b/infra/values/upc-dev/vaultwarden-values.yaml new file mode 100644 index 0000000..c53ba7f --- /dev/null +++ b/infra/values/upc-dev/vaultwarden-values.yaml @@ -0,0 +1,82 @@ +adminToken: + existingSecret: "prod-db-creds" + existingSecretKey: "adminToken" +domain: "https://vaultwarden.forteapps.net" +signupsAllowed: false +resourceType: StatefulSet +database: + type: postgresql + host: vaultwarden-postgresql + port: "5432" + dbName: vaultwarden + existingSecret: prod-db-creds + existingSecretKey: DATABASE_URL + existingSecretUserKey: pgusername + existingSecretPasswordKey: pgpassword +ingress: + enabled: true + class: "traefik" + tls: true + tlsSecret: vaultwarden-tls + hostname: vaultwarden.forteapps.net + additionalAnnotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "VaultWarden" + gethomepage.dev/description: "Password management" + gethomepage.dev/group: "Security" + gethomepage.dev/icon: "vaultwarden" + gethomepage.dev/href: "https://vaultwarden.forteapps.net" + +replicas: 1 +# Multi-Attach error for volume "pvc-102ec9a4-dccd-4cba-bb4b-650f7d934c81" Volume is already used by pod(s) vaultwarden-7f568875c7-m9cgs + +service: + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 10800 + +smtp: + host: smtp.office365.com + security: starttls + port: 587 + authMechanism: "Login" + from: noreply@fortedigital.com + fromName: "Forte Bitwarden Administrator" + debug: true + existingSecret: prod-db-creds + username: + existingSecretKey: SMTP_USERNAME + password: + existingSecretKey: SMTP_PASSWORD + +storage: + data: + name: "vaultwarden-data" + size: "5Gi" + class: "" + path: "/data" + keepPvc: true + accessMode: "ReadWriteOnce" + + attachments: + name: "vaultwarden-files" + size: "5Gi" + class: "" + path: /files + keepPvc: true + accessMode: "ReadWriteOnce" + +sso: + enabled: true + existingSecret: vaultwarden-oidc-credentials + authority: "https://id.forteapps.net/realms/forte" + scopes: "email profile" + onlySSO: true + pkce: true + signupsMatchEmail: true + clientId: + existingSecretKey: client-id + clientSecret: + existingSecretKey: client-secret