Add Argo Workflows, mTLS container registry, and fix infrastructure

- Move Keycloak off Helm to plain Crossplane Object manifests (PostgreSQL + Keycloak deployment)
- Add Vaultwarden SSO/OIDC config with Keycloak, fix Recreate deployment strategy for RWO volumes
- Switch routing from Helm-based Pomerium to pomerium-allinone with all service routes
- Deploy Argo Workflows (controller, server, CRDs, RBAC) with KEDA queue-depth autoscaling
- Add Civo cluster autoscaler with pool-scaler for zero-to-one scale-up via Civo API
- Add node-labeler to auto-tag nodes by pool membership for nodeSelector scheduling
- Set up mTLS container registry at registry.nge6.com (Forgejo built-in, client cert required)
- Add internal registry route (registry-internal.nge6.com) for in-cluster image pulls
- Fix DNS records for new Emissary LB IP (212.2.241.28)
- Fix CoreDNS crash from invalid custom config
- Fix Emissary apiext expired webhook CA certificate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Infrastructure Admin 2026-04-07 21:23:12 -04:00
parent 0b60e24c4f
commit 0dee133377
21 changed files with 2244 additions and 75 deletions

46
ambassador-listeners.yaml Normal file
View file

@ -0,0 +1,46 @@
# Ambassador Listeners - required for Ambassador 3.x
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: http-listener
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
name: http-listener
namespace: emissary
spec:
port: 8080
protocol: HTTP
securityModel: XFP
hostBinding:
namespace:
from: ALL
---
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: https-listener
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
name: https-listener
namespace: emissary
spec:
port: 8443
protocol: HTTPS
securityModel: XFP
hostBinding:
namespace:
from: ALL

View file

@ -0,0 +1,70 @@
# SSL Certificate for Argo Workflows UI
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-certificate
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: argo-tls
namespace: emissary
spec:
secretName: argo-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- workflows.nge6.com
---
# Ambassador Host for Argo UI
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-host
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
name: argo-host
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: workflows.nge6.com
tlsSecret:
name: argo-tls
---
# Ambassador Mapping for Argo UI through Pomerium
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-mapping
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
name: argo-mapping
namespace: emissary
spec:
hostname: workflows.nge6.com
prefix: /
service: http://pomerium-allinone.pomerium:443
timeout_ms: 30000
connect_timeout_ms: 10000

View file

@ -0,0 +1,440 @@
# Argo Workflows namespace
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-namespace
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Namespace
metadata:
name: argo
---
# Argo Workflows ServiceAccount
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-sa
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo
namespace: argo
---
# Argo Server ServiceAccount
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-server-sa
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo-server
namespace: argo
---
# Argo Workflows ClusterRole
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-cluster-role
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: argo-cluster-role
rules:
- apiGroups: [""]
resources: [pods, pods/exec, pods/log]
verbs: [create, get, list, watch, update, patch, delete]
- apiGroups: [""]
resources: [configmaps, secrets, services, serviceaccounts, persistentvolumeclaims, events]
verbs: [create, get, list, watch, update, patch, delete]
- apiGroups: [argoproj.io]
resources: [workflows, workflows/finalizers, workflowtemplates, workflowtemplates/finalizers, clusterworkflowtemplates, clusterworkflowtemplates/finalizers, cronworkflows, cronworkflows/finalizers, workfloweventbindings, workfloweventbindings/finalizers, workflowtaskresults, workflowartifactgctasks]
verbs: [create, get, list, watch, update, patch, delete]
- apiGroups: [argoproj.io]
resources: [workflowtasksets, workflowtasksets/status]
verbs: [create, get, list, watch, update, patch, delete]
- apiGroups: [""]
resources: [events]
verbs: [create, patch]
- apiGroups: [coordination.k8s.io]
resources: [leases]
verbs: [create, get, update]
---
# Argo Server ClusterRole
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-server-cluster-role
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: argo-server-cluster-role
rules:
- apiGroups: [""]
resources: [configmaps, events]
verbs: [get, watch, list]
- apiGroups: [""]
resources: [pods, pods/exec, pods/log]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [secrets]
verbs: [get, list, watch, create]
- apiGroups: [""]
resources: [events]
verbs: [watch, create, patch]
- apiGroups: [argoproj.io]
resources: [workflows, workflowtemplates, clusterworkflowtemplates, cronworkflows, workfloweventbindings]
verbs: [create, get, list, watch, update, patch, delete]
- apiGroups: [argoproj.io]
resources: [workflowtasksets]
verbs: [list, watch]
---
# Argo ClusterRoleBinding
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-cluster-role-binding
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-binding
subjects:
- kind: ServiceAccount
name: argo
namespace: argo
roleRef:
kind: ClusterRole
name: argo-cluster-role
apiGroup: rbac.authorization.k8s.io
---
# Argo Server ClusterRoleBinding
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-server-cluster-role-binding
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-server-binding
subjects:
- kind: ServiceAccount
name: argo-server
namespace: argo
roleRef:
kind: ClusterRole
name: argo-server-cluster-role
apiGroup: rbac.authorization.k8s.io
---
# Default workflow ServiceAccount (used by workflows themselves)
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-workflow-sa
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo-workflow
namespace: argo
---
# Workflow role - what workflows can do
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-workflow-role
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: argo-workflow-role
namespace: argo
rules:
- apiGroups: [""]
resources: [pods]
verbs: [get, watch, patch]
- apiGroups: [""]
resources: [pods/log]
verbs: [get, watch]
- apiGroups: [argoproj.io]
resources: [workflowtaskresults]
verbs: [create, patch]
---
# Workflow RoleBinding
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-workflow-role-binding
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argo-workflow-binding
namespace: argo
subjects:
- kind: ServiceAccount
name: argo-workflow
namespace: argo
roleRef:
kind: Role
name: argo-workflow-role
apiGroup: rbac.authorization.k8s.io
---
# Workflow Controller ConfigMap
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-workflow-controller-configmap
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: workflow-controller-configmap
namespace: argo
data:
config: |
workflowDefaults:
spec:
serviceAccountName: argo-workflow
imagePullSecrets:
- name: forgejo-registry
nodeSelector:
kubernetes.civo.com/node-pool: high-compute
tolerations:
- key: "kubernetes.civo.com/node-pool"
operator: "Equal"
value: "high-compute"
effect: "NoSchedule"
metricsConfig:
enabled: true
path: /metrics
port: 9090
---
# Workflow Controller Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-workflow-controller
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: workflow-controller
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: workflow-controller
template:
metadata:
labels:
app: workflow-controller
spec:
serviceAccountName: argo
containers:
- name: workflow-controller
image: quay.io/argoproj/workflow-controller:v3.6.7
args:
- --configmap
- workflow-controller-configmap
- --executor-image
- quay.io/argoproj/argoexec:v3.6.7
- --loglevel
- info
env:
- name: LEADER_ELECTION_IDENTITY
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- containerPort: 9090
name: metrics
- containerPort: 6060
name: pprof
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /healthz
port: 6060
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 6060
initialDelaySeconds: 30
periodSeconds: 30
---
# Workflow Controller Metrics Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-workflow-controller-metrics
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: workflow-controller-metrics
namespace: argo
spec:
selector:
app: workflow-controller
ports:
- name: metrics
port: 9090
targetPort: 9090
type: ClusterIP
---
# Argo Server Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-server-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: argo-server
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: argo-server
template:
metadata:
labels:
app: argo-server
spec:
serviceAccountName: argo-server
containers:
- name: argo-server
image: quay.io/argoproj/argocli:v3.6.7
args:
- server
- --auth-mode=server
- --secure=false
ports:
- containerPort: 2746
name: web
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /
port: 2746
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
---
# Argo Server Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-server-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: argo-server
namespace: argo
spec:
selector:
app: argo-server
ports:
- name: web
port: 2746
targetPort: 2746
type: ClusterIP

View file

@ -0,0 +1,78 @@
# Placeholder deployment that KEDA scales based on Argo queue depth.
# When scaled up, these pods request resources on the high-compute pool,
# triggering the Civo cluster autoscaler to add nodes.
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-queue-placeholder
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: argo-queue-placeholder
namespace: argo
spec:
replicas: 0
selector:
matchLabels:
app: argo-queue-placeholder
template:
metadata:
labels:
app: argo-queue-placeholder
spec:
nodeSelector:
kubernetes.civo.com/node-pool: high-compute
tolerations:
- key: "kubernetes.civo.com/node-pool"
operator: "Equal"
value: "high-compute"
effect: "NoSchedule"
terminationGracePeriodSeconds: 0
containers:
- name: placeholder
image: busybox
command: ["sleep", "infinity"]
resources:
requests:
cpu: "1"
memory: 1Gi
---
# KEDA ScaledObject - scales placeholder based on pending Argo workflow pods.
# When workflows are submitted, their pods land in Pending state (no nodes).
# KEDA sees the pending pods and scales up the placeholder deployment,
# which also targets high-compute nodes, adding pressure for the cluster
# autoscaler to provision new nodes.
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-queue-scaledobject
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: argo-queue-scaler
namespace: argo
spec:
scaleTargetRef:
name: argo-queue-placeholder
pollingInterval: 15
cooldownPeriod: 300
minReplicaCount: 0
maxReplicaCount: 5
triggers:
- type: kubernetes-workload
metadata:
podSelector: "workflows.argoproj.io/completed=false"
namespace: "argo"
value: "1"

View file

@ -40,6 +40,31 @@ spec:
providerConfigRef:
name: keycloak-provider
---
# Vaultwarden OIDC Client
apiVersion: openidclient.keycloak.crossplane.io/v1alpha1
kind: Client
metadata:
name: vaultwarden-client
spec:
forProvider:
realmId: kubernetes-realm
clientId: vaultwarden
name: "Vaultwarden Password Manager"
description: "Client for Vaultwarden OIDC authentication"
enabled: true
accessType: CONFIDENTIAL
clientAuthenticatorType: client-secret
validRedirectUris:
- "https://vault.nge6.com/identity/connect/oidc-signin"
- "https://vault.nge6.com/sso-connector/oidc/callback"
standardFlowEnabled: true
directAccessGrantsEnabled: false
serviceAccountsEnabled: false
webOrigins:
- "https://vault.nge6.com"
providerConfigRef:
name: keycloak-provider
---
# Create user groups
apiVersion: group.keycloak.crossplane.io/v1alpha1
kind: Group
@ -190,7 +215,7 @@ metadata:
spec:
forProvider:
realmId: kubernetes-realm
groupId: k8s-admins
groupId: 98e13ab3-0001-4646-b097-ed52ee5baff4
members: ["admin", "eemoore"]
providerConfigRef:
name: keycloak-provider
@ -202,7 +227,7 @@ metadata:
spec:
forProvider:
realmId: kubernetes-realm
groupId: users
groupId: f87d1c8e-32ee-4f63-9584-7fce67313137
members: ["admin", "eemoore"]
providerConfigRef:
name: keycloak-provider

View file

@ -97,80 +97,246 @@ spec:
stringData:
password: "thefi9paechooh"
---
# Keycloak Helm release
apiVersion: helm.crossplane.io/v1beta1
kind: Release
# PostgreSQL credentials
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak
name: keycloak-postgresql-secret
namespace: crossplane-system
spec:
providerConfigRef:
name: helm-provider
name: kubernetes-provider
forProvider:
chart:
name: keycloak
repository: https://codecentric.github.io/helm-charts
version: 18.10.0
namespace: auth-system
values:
image:
repository: quay.io/keycloak/keycloak
tag: 24.0.4
serviceAccount:
create: false
manifest:
apiVersion: v1
kind: Secret
metadata:
name: keycloak-postgresql
namespace: auth-system
type: Opaque
stringData:
postgresql-password: "keycloak-db-password"
POSTGRES_PASSWORD: "keycloak-db-password"
POSTGRES_USER: "keycloak"
POSTGRES_DB: "keycloak"
---
# PostgreSQL StatefulSet
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-postgresql-statefulset
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: keycloak-postgresql
namespace: auth-system
spec:
serviceName: keycloak-postgresql
replicas: 1
selector:
matchLabels:
app: keycloak-postgresql
template:
metadata:
labels:
app: keycloak-postgresql
spec:
containers:
- name: postgresql
image: postgres:17-bookworm
ports:
- containerPort: 5432
name: postgresql
envFrom:
- secretRef:
name: keycloak-postgresql
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
subPath: pgdata
readinessProbe:
exec:
command:
- pg_isready
- -U
- keycloak
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
exec:
command:
- pg_isready
- -U
- keycloak
initialDelaySeconds: 30
periodSeconds: 30
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: civo-volume
resources:
requests:
storage: 5Gi
---
# PostgreSQL Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-postgresql-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: keycloak-postgresql
namespace: auth-system
spec:
selector:
app: keycloak-postgresql
ports:
- name: postgresql
port: 5432
targetPort: 5432
type: ClusterIP
---
# Keycloak Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
args:
- start
- --db=postgres
- --hostname-strict=false
- --hostname-strict-https=false
- --proxy=edge
- --http-enabled=true
livenessProbe: |
httpGet:
path: /realms/master
port: http
initialDelaySeconds: 120
timeoutSeconds: 5
periodSeconds: 30
failureThreshold: 10
readinessProbe: |
httpGet:
path: /realms/master
port: http
initialDelaySeconds: 90
timeoutSeconds: 3
periodSeconds: 10
failureThreshold: 10
startupProbe: |
httpGet:
path: /realms/master
port: http
initialDelaySeconds: 60
timeoutSeconds: 3
periodSeconds: 5
failureThreshold: 30
extraEnv: |
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-admin-creds
key: password
- name: KC_DB
value: postgres
- name: KC_DB_URL
value: jdbc:postgresql://keycloak-postgresql:5432/keycloak
- name: KC_DB_USERNAME
value: keycloak
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-postgresql
key: postgresql-password
ingress:
enabled: false
namespace: auth-system
labels:
app: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
serviceAccountName: keycloak
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:24.0.4
args:
- start
- --db=postgres
- --hostname=auth.nge6.com
- --hostname-strict=false
- --hostname-strict-https=false
- --proxy=edge
- --http-enabled=true
ports:
- containerPort: 8080
name: http
env:
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-admin-creds
key: password
- name: KC_DB
value: postgres
- name: KC_DB_URL
value: jdbc:postgresql://keycloak-postgresql:5432/keycloak
- name: KC_DB_USERNAME
value: keycloak
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-postgresql
key: postgresql-password
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
readinessProbe:
httpGet:
path: /realms/master
port: 8080
initialDelaySeconds: 90
timeoutSeconds: 3
periodSeconds: 10
failureThreshold: 10
livenessProbe:
httpGet:
path: /realms/master
port: 8080
initialDelaySeconds: 120
timeoutSeconds: 5
periodSeconds: 30
failureThreshold: 10
startupProbe:
httpGet:
path: /realms/master
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 3
periodSeconds: 5
failureThreshold: 30
---
# Keycloak Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: keycloak-http
namespace: auth-system
spec:
selector:
app: keycloak
ports:
- name: http
port: 80
targetPort: 8080
type: ClusterIP
---
# Keycloak SSL Certificate
apiVersion: kubernetes.crossplane.io/v1alpha2
@ -214,6 +380,7 @@ spec:
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: auth.nge6.com
tlsSecret:

73
cluster-autoscaler.yaml Normal file
View file

@ -0,0 +1,73 @@
# Cluster autoscaler - Crossplane-managed to prevent marketplace overwriting config
# Main pool (fc94): fixed at 3 nodes
# High-compute pool (cc28): scales 0-5 based on demand
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: cluster-autoscaler-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
app: cluster-autoscaler
spec:
replicas: 1
selector:
matchLabels:
app: cluster-autoscaler
template:
metadata:
labels:
app: cluster-autoscaler
spec:
serviceAccountName: cluster-autoscaler
containers:
- name: cluster-autoscaler
image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.28.1
command:
- ./cluster-autoscaler
- --v=4
- --stderrthreshold=info
- --cloud-provider=civo
- --nodes=3:3:1b886eac-942e-40bf-8f70-7a5496f2fd3b
- --nodes=0:1:high-compute
- --skip-nodes-with-local-storage=false
- --skip-nodes-with-system-pods=false
- --scale-down-unneeded-time=5m
- --scale-down-delay-after-add=5m
env:
- name: CIVO_API_URL
valueFrom:
secretKeyRef:
key: api-url
name: civo-api-access
- name: CIVO_API_KEY
valueFrom:
secretKeyRef:
key: api-key
name: civo-api-access
- name: CIVO_CLUSTER_ID
valueFrom:
secretKeyRef:
key: cluster-id
name: civo-api-access
- name: CIVO_REGION
valueFrom:
secretKeyRef:
key: region
name: civo-api-access
resources:
requests:
cpu: 100m
memory: 300Mi
limits:
cpu: 100m
memory: 300Mi

View file

@ -47,6 +47,10 @@ spec:
LFS_START_SERVER = true
OFFLINE_MODE = false
[packages]
ENABLED = true
CONTAINER_REGISTRY_TOKEN_REALM = https://registry.nge6.com
[database]
DB_TYPE = sqlite3
PATH = /data/gitea/gitea.db
@ -305,6 +309,6 @@ spec:
spec:
hostname: git.nge6.com
prefix: /
service: https://pomerium-proxy.pomerium:443
service: http://pomerium-allinone.pomerium:443
timeout_ms: 30000
connect_timeout_ms: 10000

70
keycloak-auth-dns.yaml Normal file
View file

@ -0,0 +1,70 @@
# SSL Certificate for auth.nge6.com (Keycloak alternative)
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-auth-certificate
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: keycloak-auth-tls
namespace: emissary
spec:
secretName: keycloak-auth-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- auth.nge6.com
---
# Ambassador Host for auth.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-auth-host
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
name: keycloak-auth-host
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: auth.nge6.com
tlsSecret:
name: keycloak-auth-tls
---
# Ambassador Mapping for auth.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-auth-mapping
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
name: keycloak-auth-mapping
namespace: emissary
spec:
hostname: auth.nge6.com
prefix: /
service: keycloak-http.auth-system:80
timeout_ms: 30000
connect_timeout_ms: 10000

47
keycloak-dns-fix.yaml Normal file
View file

@ -0,0 +1,47 @@
# Fix DNS resolution for keycloak.nge6.com from inside cluster
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-internal-dns
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: keycloak-nge6-com
namespace: pomerium
spec:
type: ExternalName
externalName: keycloak-http.auth-system.svc.cluster.local
ports:
- port: 80
targetPort: 80
---
# Add custom hosts entry to CoreDNS
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: coredns-custom-hosts
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom
namespace: kube-system
data:
keycloak.override: |
keycloak.nge6.com:53 {
hosts {
212.2.241.28 keycloak.nge6.com
fallthrough
}
}

70
keycloak-nge6-dns.yaml Normal file
View file

@ -0,0 +1,70 @@
# SSL Certificate for keycloak.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-nge6-certificate
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: keycloak-nge6-tls
namespace: emissary
spec:
secretName: keycloak-nge6-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- keycloak.nge6.com
---
# Ambassador Host for keycloak.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-nge6-host
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
name: keycloak-nge6-host
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: keycloak.nge6.com
tlsSecret:
name: keycloak-nge6-tls
---
# Ambassador Mapping for keycloak.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: keycloak-nge6-mapping
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
name: keycloak-nge6-mapping
namespace: emissary
spec:
hostname: keycloak.nge6.com
prefix: /
service: keycloak-http.auth-system:80
timeout_ms: 30000
connect_timeout_ms: 10000

View file

@ -7,6 +7,7 @@ resources:
- provider-configs.yaml
- namespaces.yaml
- external-dns.yaml
- ambassador-listeners.yaml
# Certificate management
- cert-manager/
@ -20,8 +21,13 @@ resources:
# Applications
- forgejo-k8s.yaml
- pomerium.yaml
- pomerium-allinone.yaml
- pomerium-dns.yaml
- vaultwarden.yaml
- keycloak-nge6-dns.yaml
# Argo Workflows
- argo-workflows/
# Exclude problematic directories:
# - flux/ (managed by Flux itself)

127
node-labeler.yaml Normal file
View file

@ -0,0 +1,127 @@
# Node labeler - automatically labels nodes by pool based on name pattern
# fc94 in node name -> main pool (1b886eac...)
# cc28 in node name -> high-compute pool
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: node-labeler-sa
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ServiceAccount
metadata:
name: node-labeler
namespace: kube-system
---
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: node-labeler-clusterrole
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-labeler
rules:
- apiGroups: [""]
resources: [nodes]
verbs: [get, list, patch]
- apiGroups: [""]
resources: [pods]
verbs: [get, list, watch]
---
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: node-labeler-clusterrolebinding
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: node-labeler
subjects:
- kind: ServiceAccount
name: node-labeler
namespace: kube-system
roleRef:
kind: ClusterRole
name: node-labeler
apiGroup: rbac.authorization.k8s.io
---
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: node-labeler-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-labeler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: node-labeler
template:
metadata:
labels:
app: node-labeler
spec:
serviceAccountName: node-labeler
containers:
- name: labeler
image: bitnami/kubectl:latest
command:
- /bin/bash
- -c
- |
while true; do
for NODE in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
# Skip if already labeled
POOL=$(kubectl get node "$NODE" -o jsonpath='{.metadata.labels.kubernetes\.civo\.com/node-pool}' 2>/dev/null)
if [ -n "$POOL" ]; then
continue
fi
# Label based on name pattern
if echo "$NODE" | grep -q "fc94"; then
echo "Labeling $NODE as main pool"
kubectl label node "$NODE" \
kubernetes.civo.com/node-pool=1b886eac-942e-40bf-8f70-7a5496f2fd3b \
kubernetes.civo.com/node-size=g4s.kube.medium --overwrite
elif echo "$NODE" | grep -q "cc28"; then
echo "Labeling $NODE as high-compute pool"
kubectl label node "$NODE" \
kubernetes.civo.com/node-pool=high-compute \
kubernetes.civo.com/node-size=g4c.kube.small --overwrite
fi
done
sleep 30
done
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 50m
memory: 64Mi

181
pomerium-allinone.yaml Normal file
View file

@ -0,0 +1,181 @@
# Pomerium All-In-One Deployment (single process, no Helm)
# ConfigMap for Pomerium configuration
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-allinone-config
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: pomerium-allinone
namespace: pomerium
data:
config.yaml: |
# Core configuration
address: :443
http_redirect_addr: :80
# Security keys (32 bytes base64)
shared_secret: 5Cz7gj71G5ujzH9HIc1XgwabUXCdJ3st9649gNlknrI=
cookie_secret: SXzBgU9L72OI+QCD9lEOxXcjApyE+4oIbetqtveNcjc=
# Run in insecure mode (no TLS certs required)
insecure_server: true
# Service URLs (internal)
authenticate_service_url: https://authenticate.nge6.com
# Identity provider
idp_provider: oidc
idp_provider_url: https://auth.nge6.com/realms/kubernetes-realm
idp_client_id: pomerium
idp_client_secret: U3Elh0oZEazKRpHpIasgP8yovUGsvq5K
idp_scopes:
- openid
- profile
- email
# Routes
routes:
# Keycloak admin
- from: https://keycloak.nge6.com
to: http://keycloak-http.auth-system.svc.cluster.local
preserve_host_header: true
allow_public_unauthenticated_access: true
# Vaultwarden SSO/OAuth endpoints only (needed for authentication flow)
- from: https://vault.nge6.com
to: http://vaultwarden-http.vaultwarden.svc.cluster.local:8080
prefix: /identity/connect
preserve_host_header: true
allow_public_unauthenticated_access: true
- from: https://vault.nge6.com
to: http://vaultwarden-http.vaultwarden.svc.cluster.local:8080
prefix: /identity/sso
preserve_host_header: true
allow_public_unauthenticated_access: true
# Vaultwarden API endpoints (protected by Vaultwarden's own auth)
- from: https://vault.nge6.com
to: http://vaultwarden-http.vaultwarden.svc.cluster.local:8080
prefix: /api
preserve_host_header: true
allow_public_unauthenticated_access: true
# Vaultwarden web UI - requires Pomerium authentication
- from: https://vault.nge6.com
to: http://vaultwarden-http.vaultwarden.svc.cluster.local:8080
preserve_host_header: true
allow_any_authenticated_user: true
# Forgejo container registry token endpoint (Docker auth)
- from: https://git.nge6.com
to: http://forgejo-http.forgejo.svc.cluster.local:3000
prefix: /v2/token
preserve_host_header: true
allow_public_unauthenticated_access: true
# Forgejo Git - requires authentication
- from: https://git.nge6.com
to: http://forgejo-http.forgejo.svc.cluster.local:3000
preserve_host_header: true
allow_any_authenticated_user: true
# Argo Workflows UI - requires authentication
- from: https://workflows.nge6.com
to: http://argo-server.argo.svc.cluster.local:2746
preserve_host_header: true
allow_any_authenticated_user: true
---
# Pomerium All-In-One Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-allinone-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pomerium-allinone
namespace: pomerium
spec:
replicas: 1
selector:
matchLabels:
app: pomerium-allinone
template:
metadata:
labels:
app: pomerium-allinone
spec:
containers:
- name: pomerium
image: pomerium/pomerium:v0.25.0
args:
- --config=/etc/pomerium/config.yaml
env:
# Run all services in one container
- name: SERVICES
value: all
- name: INSECURE_SERVER
value: "true"
ports:
- containerPort: 443
name: https
- containerPort: 80
name: http
volumeMounts:
- name: config
mountPath: /etc/pomerium
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
volumes:
- name: config
configMap:
name: pomerium-allinone
---
# Pomerium All-In-One Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-allinone-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: pomerium-allinone
namespace: pomerium
spec:
selector:
app: pomerium-allinone
ports:
- name: https
port: 443
targetPort: 443
- name: http
port: 80
targetPort: 80

71
pomerium-dns.yaml Normal file
View file

@ -0,0 +1,71 @@
# DNS and SSL for Pomerium authenticate endpoint
# SSL Certificate for authenticate.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authenticate-certificate
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pomerium-authenticate-tls
namespace: emissary
spec:
secretName: pomerium-authenticate-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- authenticate.nge6.com
---
# Ambassador Host for authenticate.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authenticate-host
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
name: pomerium-authenticate-host
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: authenticate.nge6.com
tlsSecret:
name: pomerium-authenticate-tls
---
# Ambassador Mapping for authenticate.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authenticate-mapping
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
name: pomerium-authenticate-mapping
namespace: emissary
spec:
hostname: authenticate.nge6.com
prefix: /
service: http://pomerium-allinone.pomerium:443
timeout_ms: 30000
connect_timeout_ms: 10000

397
pomerium-native.yaml Normal file
View file

@ -0,0 +1,397 @@
# Pomerium Native Kubernetes Deployment (No Helm!)
# Namespace already exists from previous deployment
# ConfigMap for Pomerium configuration
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-config
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: pomerium
namespace: pomerium
data:
config.yaml: |
# Core configuration
address: :443
grpc_address: :5443
# Security keys
shared_secret: 5Cz7gj71G5ujzH9HIc1XgwabUXCdJ3st9649gNlknrI=
cookie_secret: SXzBgU9L72OI+QCD9lEOxXcjApyE+4oIbetqtveNcjc=
# Service URLs
authenticate_service_url: https://authenticate.nge6.com
authorize_service_url: http://pomerium-authorize.pomerium.svc.cluster.local:5443
databroker_service_url: http://pomerium-databroker.pomerium.svc.cluster.local:5443
# Run in insecure mode for internal cluster communication
insecure_server: true
# Identity provider
idp_provider: oidc
idp_provider_url: https://keycloak.nge6.com/realms/kubernetes-realm
idp_client_id: pomerium
idp_client_secret: 3JFMh3DZDOYlNiSQ64abL0z0bw1WJt3x
idp_scopes:
- openid
- profile
- email
# Routes
routes:
# Keycloak admin (public for initial setup)
- from: https://keycloak.nge6.com
to: http://keycloak-http.auth-system.svc.cluster.local
preserve_host_header: true
allow_public_unauthenticated_access: true
# Vaultwarden - requires authentication
- from: https://vault.nge6.com
to: http://vaultwarden-http.vaultwarden.svc.cluster.local:8080
preserve_host_header: true
allow_any_authenticated_user: true
# Forgejo Git - requires authentication
- from: https://git.nge6.com
to: http://forgejo-http.forgejo.svc.cluster.local:3000
preserve_host_header: true
allow_any_authenticated_user: true
# Authentication endpoint
- from: https://authenticate.nge6.com
to: http://pomerium-authenticate.pomerium.svc.cluster.local
allow_public_unauthenticated_access: true
---
# Pomerium Authenticate Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authenticate-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pomerium-authenticate
namespace: pomerium
spec:
replicas: 1
selector:
matchLabels:
app: pomerium-authenticate
template:
metadata:
labels:
app: pomerium-authenticate
spec:
containers:
- name: pomerium
image: pomerium/pomerium:v0.25.0
args:
- --config=/etc/pomerium/config.yaml
env:
- name: SERVICES
value: authenticate
- name: INSECURE_SERVER
value: "true"
- name: ADDRESS
value: :80
- name: GRPC_ADDRESS
value: :5443
- name: GRPC_INSECURE
value: "true"
ports:
- containerPort: 80
name: http
- containerPort: 5443
name: grpc
volumeMounts:
- name: config
mountPath: /etc/pomerium
volumes:
- name: config
configMap:
name: pomerium
---
# Pomerium Authenticate Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authenticate-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: pomerium-authenticate
namespace: pomerium
spec:
selector:
app: pomerium-authenticate
ports:
- name: http
port: 80
targetPort: 80
- name: grpc
port: 5443
targetPort: 5443
---
# Pomerium Authorize Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authorize-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pomerium-authorize
namespace: pomerium
spec:
replicas: 1
selector:
matchLabels:
app: pomerium-authorize
template:
metadata:
labels:
app: pomerium-authorize
spec:
containers:
- name: pomerium
image: pomerium/pomerium:v0.25.0
args:
- --config=/etc/pomerium/config.yaml
env:
- name: SERVICES
value: authorize
- name: INSECURE_SERVER
value: "true"
- name: ADDRESS
value: :80
- name: GRPC_ADDRESS
value: :5443
- name: GRPC_INSECURE
value: "true"
ports:
- containerPort: 80
name: http
- containerPort: 5443
name: grpc
volumeMounts:
- name: config
mountPath: /etc/pomerium
volumes:
- name: config
configMap:
name: pomerium
---
# Pomerium Authorize Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-authorize-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: pomerium-authorize
namespace: pomerium
spec:
selector:
app: pomerium-authorize
ports:
- name: http
port: 80
targetPort: 80
- name: grpc
port: 5443
targetPort: 5443
---
# Pomerium Databroker Deployment
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-databroker-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pomerium-databroker
namespace: pomerium
spec:
replicas: 1
selector:
matchLabels:
app: pomerium-databroker
template:
metadata:
labels:
app: pomerium-databroker
spec:
containers:
- name: pomerium
image: pomerium/pomerium:v0.25.0
args:
- --config=/etc/pomerium/config.yaml
env:
- name: SERVICES
value: databroker
- name: INSECURE_SERVER
value: "true"
- name: ADDRESS
value: :80
- name: GRPC_ADDRESS
value: :5443
- name: GRPC_INSECURE
value: "true"
ports:
- containerPort: 80
name: http
- containerPort: 5443
name: grpc
volumeMounts:
- name: config
mountPath: /etc/pomerium
volumes:
- name: config
configMap:
name: pomerium
---
# Pomerium Databroker Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-databroker-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: pomerium-databroker
namespace: pomerium
spec:
selector:
app: pomerium-databroker
ports:
- name: http
port: 80
targetPort: 80
- name: grpc
port: 5443
targetPort: 5443
---
# Pomerium Proxy Deployment (the main ingress point)
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-proxy-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pomerium-proxy
namespace: pomerium
spec:
replicas: 1
selector:
matchLabels:
app: pomerium-proxy
template:
metadata:
labels:
app: pomerium-proxy
spec:
containers:
- name: pomerium
image: pomerium/pomerium:v0.25.0
args:
- --config=/etc/pomerium/config.yaml
env:
- name: SERVICES
value: proxy
- name: INSECURE_SERVER
value: "true"
- name: ADDRESS
value: :443
- name: HTTP_REDIRECT_ADDR
value: :80
ports:
- containerPort: 443
name: https
- containerPort: 80
name: http
volumeMounts:
- name: config
mountPath: /etc/pomerium
volumes:
- name: config
configMap:
name: pomerium
---
# Pomerium Proxy Service
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pomerium-proxy-service
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Service
metadata:
name: pomerium-proxy
namespace: pomerium
spec:
selector:
app: pomerium-proxy
ports:
- name: https
port: 443
targetPort: 443
- name: http
port: 80
targetPort: 80

View file

@ -41,6 +41,11 @@ spec:
to: http://forgejo-http.forgejo.svc.cluster.local:3000
preserve_host_header: true
allow_any_authenticated_user: true
# Vaultwarden password manager - require authentication
- from: https://vault.nge6.com
to: http://vaultwarden-http.vaultwarden.svc.cluster.local:8080
preserve_host_header: true
allow_any_authenticated_user: true
# Authentication service configuration
authenticate:

91
pool-scaler.yaml Normal file
View file

@ -0,0 +1,91 @@
# Pool scaler - watches for pending Argo workflow pods and scales the
# high-compute pool via the Civo API. Handles scale-up from zero since
# the cluster autoscaler can't do this without a node template.
# The cluster autoscaler still handles scale-down.
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: pool-scaler-deployment
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pool-scaler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: pool-scaler
template:
metadata:
labels:
app: pool-scaler
spec:
serviceAccountName: node-labeler
containers:
- name: scaler
image: bitnami/kubectl:latest
command:
- /bin/bash
- -c
- |
echo "Pool scaler started. Watching for pending workflow pods..."
while true; do
# Count pending pods with high-compute nodeSelector
PENDING=$(kubectl get pods -n argo --field-selector=status.phase=Pending \
-o jsonpath='{.items[*].spec.nodeSelector}' 2>/dev/null \
| tr '}' '\n' | grep -c 'high-compute' || true)
if [ "$PENDING" -gt 0 ]; then
# Check if any high-compute nodes already exist
HC_NODES=$(kubectl get nodes -l kubernetes.civo.com/node-pool=high-compute --no-headers 2>/dev/null | wc -l)
if [ "$HC_NODES" -eq 0 ]; then
echo "$(date): $PENDING pending workflow pods, no high-compute nodes. Scaling pool to 1..."
curl -s -X PUT \
"${CIVO_API_URL}/v2/kubernetes/clusters/${CIVO_CLUSTER_ID}/pools/high-compute" \
-H "Authorization: bearer ${CIVO_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"count\": 1, \"region\": \"${CIVO_REGION}\"}"
echo ""
# Wait for node to provision before checking again
sleep 180
fi
fi
sleep 15
done
env:
- name: CIVO_API_URL
valueFrom:
secretKeyRef:
key: api-url
name: civo-api-access
- name: CIVO_API_KEY
valueFrom:
secretKeyRef:
key: api-key
name: civo-api-access
- name: CIVO_CLUSTER_ID
valueFrom:
secretKeyRef:
key: cluster-id
name: civo-api-access
- name: CIVO_REGION
valueFrom:
secretKeyRef:
key: region
name: civo-api-access
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 50m
memory: 64Mi

92
registry-internal.yaml Normal file
View file

@ -0,0 +1,92 @@
# Internal registry access - no mTLS, Forgejo handles auth via imagePullSecret
# Only accessible via cluster-internal DNS (no external-dns annotation)
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-internal-certificate
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: registry-internal-tls
namespace: emissary
spec:
secretName: registry-internal-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- registry-internal.nge6.com
---
# Host without external-dns - only reachable if you know the IP
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-internal-host
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v2
kind: Host
metadata:
name: registry-internal-host
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: registry-internal.nge6.com
tlsSecret:
name: registry-internal-tls
---
# Mapping direct to Forgejo - no Pomerium, no mTLS
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-internal-mapping
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v2
kind: Mapping
metadata:
name: registry-internal-mapping
namespace: emissary
spec:
host: registry-internal.nge6.com
prefix: /
service: http://forgejo-http.forgejo.svc.cluster.local:3000
timeout_ms: 300000
connect_timeout_ms: 10000
---
# imagePullSecret for argo namespace
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: argo-registry-pull-secret
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: v1
kind: Secret
metadata:
name: forgejo-registry
namespace: argo
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: |
{"auths":{"registry-internal.nge6.com":{"username":"eemoore","password":"testpassword123!","auth":"ZWVtb29yZTp0ZXN0cGFzc3dvcmQxMjMh"}}}

97
registry.yaml Normal file
View file

@ -0,0 +1,97 @@
# SSL Certificate for registry.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-certificate
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: registry-tls
namespace: emissary
spec:
secretName: registry-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- registry.nge6.com
---
# Ambassador Host for registry.nge6.com
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-host
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v2
kind: Host
metadata:
name: registry-host
namespace: emissary
annotations:
external-dns.ambassador-service: emissary-ingress.emissary.svc.cluster.local
external-dns.alpha.kubernetes.io/target: 212.2.241.28
spec:
hostname: registry.nge6.com
tlsSecret:
name: registry-tls
tlsContext:
name: registry-mtls
---
# TLSContext for mTLS - requires client certificates
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-tls-context
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v2
kind: TLSContext
metadata:
name: registry-mtls
namespace: emissary
spec:
hosts:
- registry.nge6.com
secret: registry-tls
ca_secret: registry-client-ca
cert_required: true
min_tls_version: v1.2
max_tls_version: v1.2
---
# Ambassador Mapping for registry - direct to Forgejo, no Pomerium
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
metadata:
name: registry-mapping
namespace: crossplane-system
spec:
providerConfigRef:
name: kubernetes-provider
forProvider:
manifest:
apiVersion: getambassador.io/v2
kind: Mapping
metadata:
name: registry-mapping
namespace: emissary
spec:
host: registry.nge6.com
prefix: /
service: http://forgejo-http.forgejo.svc.cluster.local:3000
timeout_ms: 300000
connect_timeout_ms: 10000

View file

@ -37,9 +37,19 @@ spec:
ROCKET_WORKERS: "10"
# Security settings
INVITATIONS_ALLOWED: "true"
SIGNUPS_ALLOWED: "false"
SIGNUPS_ALLOWED: "true"
SHOW_PASSWORD_HINT: "false"
# Email configuration (disabled)
# OIDC/SSO configuration
SSO_ENABLED: "true"
SSO_ONLY: "false"
SSO_CLIENT_ID: "vaultwarden"
SSO_CLIENT_SECRET: "zMeG3odq6GUBoYUVcoNl1CmngJpwgMS6"
SSO_AUTHORITY: "https://auth.nge6.com/realms/kubernetes-realm"
SSO_SCOPES: "openid email profile"
# SSO_MASTER_PASSWORD_POLICY removed - not valid in testing image
SSO_DOMAIN: "nge6.com"
SSO_ORGANIZATIONS_INVITE: "true"
# Admin settings
ADMIN_TOKEN: "vaultwarden-admin-token-change-in-production"
# Database (using SQLite for simplicity)
@ -93,6 +103,8 @@ spec:
app: vaultwarden
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: vaultwarden
@ -103,7 +115,7 @@ spec:
spec:
containers:
- name: vaultwarden
image: vaultwarden/server:1.30.5
image: vaultwarden/server:testing
ports:
- containerPort: 8080
name: http
@ -233,6 +245,6 @@ spec:
spec:
hostname: vault.nge6.com
prefix: /
service: https://pomerium-proxy.pomerium:443
service: http://pomerium-allinone.pomerium:443
timeout_ms: 30000
connect_timeout_ms: 10000