diff --git a/apps/base/netbox/cnpg_backup.yaml b/apps/base/netbox/cnpg_backup.yaml new file mode 100644 index 0000000..6069e9a --- /dev/null +++ b/apps/base/netbox/cnpg_backup.yaml @@ -0,0 +1,49 @@ +--- +# Ceph RGW (S3) backup target for the netbox CNPG cluster, provisioned by the +# in-estate cephrgw-operator. One dedicated bucket + owner user per cluster: +# cephrgw CRs are namespace-scoped and CNPG reads its S3 credential Secret from +# its own namespace, so backups are per-database rather than one shared bucket. +apiVersion: ceph.unkin.net/v1alpha1 +kind: ObjectStoreUser +metadata: + name: cnpg-netbox-backup + namespace: netbox +spec: + displayName: "CNPG backup owner (netbox)" + # RGW users are global; keep the uid namespace-qualified so it never collides. + uid: cnpg-netbox-backup + maxBuckets: 5 + # Operator writes AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (+ RGW_UID, + # S3_ENDPOINT) into this Secret; the Cluster's barmanObjectStore consumes it. + secretName: cnpg-netbox-backup-s3 + # Keep the RGW user (and thus the keys) if this CR is ever deleted. + retainOnDelete: true +--- +apiVersion: ceph.unkin.net/v1alpha1 +kind: Bucket +metadata: + name: cnpg-netbox + namespace: netbox +spec: + bucketName: cnpg-netbox + ownerRef: cnpg-netbox-backup + versioning: false + tags: + app: netbox + purpose: cnpg-backup + retainOnDelete: true +--- +apiVersion: postgresql.cnpg.io/v1 +kind: ScheduledBackup +metadata: + name: cnpg-netbox-nightly + namespace: netbox +spec: + # 6-field CNPG cron (seconds first). 03:40 — next free slot after grafana + # (03:20), keeping the estate's 20-minute stagger. + schedule: "0 40 3 * * *" + immediate: false + backupOwnerReference: self + method: barmanObjectStore + cluster: + name: netbox-postgres diff --git a/apps/base/netbox/cnpg_cluster.yaml b/apps/base/netbox/cnpg_cluster.yaml new file mode 100644 index 0000000..f9a0266 --- /dev/null +++ b/apps/base/netbox/cnpg_cluster.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: netbox-postgres + namespace: netbox +spec: + affinity: + podAntiAffinityType: preferred + backup: + # 30-day retention (DEFAULT — adjust per cluster if needed). Enforced by CNPG + # against the object store on each successful base backup. + retentionPolicy: 30d + barmanObjectStore: + # Dedicated per-cluster Ceph RGW bucket (cephrgw-operator provisions it). + destinationPath: s3://cnpg-netbox + endpointURL: https://s3.ceph.unkin.net + # radosgw serves a Vault-PKI cert; trust the internal CA (reflected into + # every namespace as the vault-ca-cert Secret). + endpointCA: + name: vault-ca-cert + key: ca.crt + # Keys minted by the ObjectStoreUser in cnpg_backup.yaml; never hardcoded. + s3Credentials: + accessKeyId: + name: cnpg-netbox-backup-s3 + key: AWS_ACCESS_KEY_ID + secretAccessKey: + name: cnpg-netbox-backup-s3 + key: AWS_SECRET_ACCESS_KEY + # Path prefix within the bucket; keep stable across restores (see docs). + serverName: netbox + data: + compression: bzip2 + jobs: 2 + wal: + compression: zstd + maxParallel: 2 + bootstrap: + initdb: + database: netbox + encoding: UTF8 + localeCType: C + localeCollate: C + owner: netbox + secret: + name: postgres-credentials + enablePDB: true + enableSuperuserAccess: false + failoverDelay: 0 + imageName: ghcr.io/cloudnative-pg/postgresql:18.1-system-trixie + instances: 2 + logLevel: info + monitoring: + customQueriesConfigMap: + - key: queries + name: cnpg-default-monitoring + disableDefaultQueries: false + enablePodMonitor: false + postgresql: + parameters: + max_connections: "200" + shared_buffers: 128MB + primaryUpdateMethod: restart + primaryUpdateStrategy: unsupervised + replicationSlots: + highAvailability: + enabled: true + slotPrefix: _cnpg_ + synchronizeReplicas: + enabled: true + updateInterval: 30 + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 100m + memory: 512Mi + smartShutdownTimeout: 180 + startDelay: 3600 + stopDelay: 1800 + storage: + resizeInUseVolumes: true + size: 10Gi + storageClass: cephrbd-fast-delete + switchoverDelay: 3600 diff --git a/apps/base/netbox/cnpg_pooler.yaml b/apps/base/netbox/cnpg_pooler.yaml new file mode 100644 index 0000000..fb96390 --- /dev/null +++ b/apps/base/netbox/cnpg_pooler.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Pooler +metadata: + name: netbox-postgres-pooler-rw + namespace: netbox +spec: + cluster: + name: netbox-postgres + instances: 2 + pgbouncer: + parameters: + default_pool_size: "50" + max_client_conn: "200" + paused: false + # Session mode: NetBox/Django server-side cursors work unchanged. + poolMode: session + template: + metadata: + labels: + app: pooler-rw + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - pooler-rw + topologyKey: kubernetes.io/hostname + containers: [] + type: rw diff --git a/apps/base/netbox/gateway.yaml b/apps/base/netbox/gateway.yaml new file mode 100644 index 0000000..0a68fdf --- /dev/null +++ b/apps/base/netbox/gateway.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: netbox + namespace: netbox + labels: + app.kubernetes.io/name: netbox + app.kubernetes.io/instance: netbox + traefik.io/instance: internal + annotations: + cert-manager.io/cluster-issuer: vault-issuer + cert-manager.io/common-name: netbox.k8s.syd1.au.unkin.net + cert-manager.io/private-key-size: "4096" + external-dns.alpha.kubernetes.io/hostname: netbox.k8s.syd1.au.unkin.net + external-dns.alpha.kubernetes.io/target: 198.18.200.4 +spec: + gatewayClassName: traefik-internal + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: netbox.k8s.syd1.au.unkin.net + allowedRoutes: + namespaces: + from: Same + - name: https + port: 443 + protocol: HTTPS + hostname: netbox.k8s.syd1.au.unkin.net + allowedRoutes: + namespaces: + from: Same + tls: + mode: Terminate + certificateRefs: + - group: "" + kind: Secret + name: netbox-tls diff --git a/apps/base/netbox/httproute.yaml b/apps/base/netbox/httproute.yaml new file mode 100644 index 0000000..c655fc7 --- /dev/null +++ b/apps/base/netbox/httproute.yaml @@ -0,0 +1,55 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: netbox-http-redirect + namespace: netbox + labels: + app.kubernetes.io/name: netbox + app.kubernetes.io/instance: netbox +spec: + hostnames: + - netbox.k8s.syd1.au.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: netbox + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + matches: + - path: + type: PathPrefix + value: / +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: netbox + namespace: netbox + labels: + app.kubernetes.io/name: netbox + app.kubernetes.io/instance: netbox +spec: + hostnames: + - netbox.k8s.syd1.au.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: netbox + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: netbox + port: 80 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/apps/base/netbox/kustomization.yaml b/apps/base/netbox/kustomization.yaml new file mode 100644 index 0000000..03a8b42 --- /dev/null +++ b/apps/base/netbox/kustomization.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - cnpg_cluster.yaml + - cnpg_backup.yaml + - cnpg_pooler.yaml + - valkey-deployment.yaml + - valkey-pvc.yaml + - valkey-service.yaml + - vaultauth.yaml + - vaultstaticsecret.yaml + - gateway.yaml + - httproute.yaml diff --git a/apps/base/netbox/namespace.yaml b/apps/base/netbox/namespace.yaml new file mode 100644 index 0000000..66fd51f --- /dev/null +++ b/apps/base/netbox/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/name: netbox + name: netbox diff --git a/apps/base/netbox/valkey-deployment.yaml b/apps/base/netbox/valkey-deployment.yaml new file mode 100644 index 0000000..52f04cf --- /dev/null +++ b/apps/base/netbox/valkey-deployment.yaml @@ -0,0 +1,89 @@ +--- +# Standalone Valkey (Redis-compatible) for NetBox. The netbox-community chart +# bundles a Bitnami Valkey subchart, but we run our own here: it keeps image +# control in-estate (valkey/valkey, already allowlisted through the artifactapi +# dockerhub mirror), avoids the Bitnami "legacy image" churn, and matches the +# standalone-cache pattern used by litellm. One instance serves both NetBox +# databases: DB 0 = RQ task queue, DB 1 = cache. AOF persistence is enabled so +# queued background jobs survive a restart. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: netbox-valkey + namespace: netbox + labels: + app.kubernetes.io/name: netbox + app.kubernetes.io/component: valkey +spec: + replicas: 1 + selector: + matchLabels: + app: netbox-valkey + strategy: + type: Recreate + template: + metadata: + labels: + app: netbox-valkey + app.kubernetes.io/name: netbox + app.kubernetes.io/component: valkey + spec: + securityContext: + fsGroup: 999 + containers: + - name: valkey + image: valkey/valkey:8-alpine + imagePullPolicy: IfNotPresent + command: + - valkey-server + - --appendonly + - "yes" + - --save + - "60" + - "1" + ports: + - containerPort: 6379 + name: valkey + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 999 + capabilities: + drop: + - ALL + livenessProbe: + exec: + command: + - valkey-cli + - ping + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - valkey-cli + - ping + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + volumeMounts: + - mountPath: /data + name: data + restartPolicy: Always + volumes: + - name: data + persistentVolumeClaim: + claimName: netbox-valkey-data diff --git a/apps/base/netbox/valkey-pvc.yaml b/apps/base/netbox/valkey-pvc.yaml new file mode 100644 index 0000000..ac65987 --- /dev/null +++ b/apps/base/netbox/valkey-pvc.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: netbox-valkey-data + namespace: netbox +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: cephrbd-fast-delete + volumeMode: Filesystem diff --git a/apps/base/netbox/valkey-service.yaml b/apps/base/netbox/valkey-service.yaml new file mode 100644 index 0000000..1d4994a --- /dev/null +++ b/apps/base/netbox/valkey-service.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: netbox-valkey + namespace: netbox + labels: + app.kubernetes.io/name: netbox + app.kubernetes.io/component: valkey +spec: + internalTrafficPolicy: Cluster + ports: + - name: valkey + port: 6379 + protocol: TCP + targetPort: valkey + selector: + app: netbox-valkey + sessionAffinity: None + type: ClusterIP diff --git a/apps/base/netbox/vaultauth.yaml b/apps/base/netbox/vaultauth.yaml new file mode 100644 index 0000000..f9ebcce --- /dev/null +++ b/apps/base/netbox/vaultauth.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +metadata: + name: default + namespace: netbox +spec: + allowedNamespaces: + - netbox + kubernetes: + audiences: + - vault + role: default + serviceAccount: default + tokenExpirationSeconds: 600 + method: kubernetes + mount: k8s/au/syd1 + vaultConnectionRef: vso-system/default diff --git a/apps/base/netbox/vaultstaticsecret.yaml b/apps/base/netbox/vaultstaticsecret.yaml new file mode 100644 index 0000000..59833d3 --- /dev/null +++ b/apps/base/netbox/vaultstaticsecret.yaml @@ -0,0 +1,81 @@ +--- +# CNPG app-user credentials (keys: username, password). Consumed by the Cluster +# bootstrap (initdb.secret) AND by NetBox (externalDatabase.existingSecretName). +# One-time Vault seed — see the PR description. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: postgres-credentials + namespace: netbox +spec: + destination: + create: true + name: postgres-credentials + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/netbox/default/postgres-credentials + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default +--- +# Django SECRET_KEY (key: secret_key). One-time Vault seed. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: netbox-secret-key + namespace: netbox +spec: + destination: + create: true + name: netbox-secret-key + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/netbox/default/netbox-secret-key + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default +--- +# Bootstrap superuser (keys: username, password, email, api_token). Applied by +# NetBox on first boot. One-time Vault seed. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: netbox-superuser + namespace: netbox +spec: + destination: + create: true + name: netbox-superuser + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/netbox/default/netbox-superuser + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default +--- +# Authentik OIDC client secret. Two keys at the same Vault path: +# client_secret : raw secret, read by the terraform-authentik provider runner +# (policy already grants kv/.../namespace/+/default/oauth-credentials). +# oidc.yaml : a YAML fragment 'SOCIAL_AUTH_OIDC_SECRET: ""', mounted +# into NetBox via values.yaml extraConfig (the chart's config +# loader globs /run/config/extra/*/*.yaml). +# One-time Vault seed — see the PR description. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: oauth-credentials + namespace: netbox +spec: + destination: + create: true + name: oauth-credentials + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/netbox/default/oauth-credentials + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/overlays/au-syd1/netbox/kustomization.yaml b/apps/overlays/au-syd1/netbox/kustomization.yaml new file mode 100644 index 0000000..fdc067b --- /dev/null +++ b/apps/overlays/au-syd1/netbox/kustomization.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/netbox + +helmCharts: + - name: netbox + repo: oci://ghcr.io/netbox-community/netbox-chart + version: "8.3.40" + releaseName: netbox + namespace: netbox + valuesFile: values.yaml + +# The chart always renders a `helm.sh/hook: test` connection Pod. We deploy via +# kustomize+ArgoCD (not `helm test`), so drop it rather than leave an orphan Pod. +patches: + - target: + version: v1 + kind: Pod + name: netbox-test-connection + patch: |- + $patch: delete + apiVersion: v1 + kind: Pod + metadata: + name: netbox-test-connection diff --git a/apps/overlays/au-syd1/netbox/values.yaml b/apps/overlays/au-syd1/netbox/values.yaml new file mode 100644 index 0000000..6d5665a --- /dev/null +++ b/apps/overlays/au-syd1/netbox/values.yaml @@ -0,0 +1,101 @@ +--- +# NetBox (netbox-community chart 8.3.40, appVersion v4.6.5). +# Postgres = external CNPG cluster, Valkey = standalone Deployment, both defined +# in apps/base/netbox. Secrets come from Vault via VSO (see base/vaultstaticsecret.yaml). + +image: + # ghcr.io is mirrored through artifactapi at the containerd level; the pull is + # gated by the ^netbox-community/ allowlist added in terraform-artifactapi. + # tag defaults to the chart appVersion (v4.6.5). + registry: ghcr.io + repository: netbox-community/netbox + +# Two web replicas for zero-downtime rollouts (media is RWX, see below). +replicaCount: 2 + +# Django SECRET_KEY — existingSecret must contain key: secret_key. +existingSecret: netbox-secret-key + +# Bootstrap superuser — existingSecret keys: username, password, email, api_token. +superuser: + existingSecret: netbox-superuser + +# Disable the bundled Bitnami subcharts; we bring our own Postgres and Valkey. +postgresql: + enabled: false +valkey: + enabled: false + +# External Postgres = the CNPG cluster's pgbouncer pooler (session mode). +externalDatabase: + host: netbox-postgres-pooler-rw + port: 5432 + database: netbox + username: netbox + existingSecretName: postgres-credentials + existingSecretKey: password + disableServerSideCursors: false + +# External Valkey — one instance, DB 0 = RQ task queue, DB 1 = cache. No auth +# (in-cluster, namespace-isolated). +tasksDatabase: + host: netbox-valkey + port: 6379 + database: 0 +cachingDatabase: + host: netbox-valkey + port: 6379 + database: 1 + +# Uploaded media/attachments, shared across web replicas (RWX CephFS). +persistence: + enabled: true + storageClass: cephfs-raid6-delete + accessMode: ReadWriteMany + size: 5Gi + +allowedHosts: + - netbox.k8s.syd1.au.unkin.net + +serviceAccount: + create: true + automountServiceAccountToken: false + +resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: "1" + memory: 2Gi + +worker: + replicaCount: 1 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + +# --- Authentik OIDC SSO via python-social-auth --- +# remoteAuth wires REMOTE_AUTH_ENABLED + REMOTE_AUTH_BACKEND; the SOCIAL_AUTH_* +# settings are supplied via extraConfig (YAML fragments the chart loads into the +# NetBox config). New OIDC users are auto-provisioned on first login. +remoteAuth: + enabled: true + backends: + - social_core.backends.open_id_connect.OpenIdConnectAuth + autoCreateUser: true +extraConfig: + # index 0 -> /run/config/extra/0/extra-0.yaml (non-secret OIDC config) + - values: + SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: https://identity.k8s.syd1.au.unkin.net/application/o/netbox/ + SOCIAL_AUTH_OIDC_KEY: netbox + # index 1 -> /run/config/extra/1/oidc.yaml (client secret, from Vault via VSO) + - secret: + secretName: oauth-credentials + items: + - key: oidc.yaml + path: oidc.yaml diff --git a/argocd/applicationsets/platform.yaml b/argocd/applicationsets/platform.yaml index 84f67c7..43194d8 100644 --- a/argocd/applicationsets/platform.yaml +++ b/argocd/applicationsets/platform.yaml @@ -28,6 +28,7 @@ spec: - path: apps/overlays/*/inteldeviceplugins-system - path: apps/overlays/*/jfrog - path: apps/overlays/*/kanidm + - path: apps/overlays/*/netbox - path: apps/overlays/*/node-feature-discovery - path: apps/overlays/*/pdbmux - path: apps/overlays/*/priority-classes diff --git a/argocd/projects/platform.yaml b/argocd/projects/platform.yaml index ac32f91..27f469b 100644 --- a/argocd/projects/platform.yaml +++ b/argocd/projects/platform.yaml @@ -12,6 +12,7 @@ spec: - https://purelb.github.io/purelb/charts - oci://gcr.io/k8s-staging-nfd/charts - oci://ghcr.io/woodpecker-ci/helm/woodpecker + - oci://ghcr.io/netbox-community/netbox-chart/netbox destinations: - namespace: '*-system' server: https://kubernetes.default.svc @@ -37,6 +38,8 @@ spec: server: https://kubernetes.default.svc - namespace: 'kanidm' server: https://kubernetes.default.svc + - namespace: 'netbox' + server: https://kubernetes.default.svc - namespace: 'node-feature-discovery' server: https://kubernetes.default.svc - namespace: 'pdbmux'