diff --git a/charts/patchmon/Chart.yaml b/charts/patchmon/Chart.yaml index ea6919d..578f872 100644 --- a/charts/patchmon/Chart.yaml +++ b/charts/patchmon/Chart.yaml @@ -1,9 +1,9 @@ apiVersion: v2 name: patchmon -description: PatchMon (backend + frontend) with optional Gateway API, Postgres, and Valkey +description: PatchMon v2 – Linux patch management platform with optional Gateway API, Postgres, Valkey, and Guacd type: application -version: 0.2.10 -appVersion: "1.4.0" +version: 0.3.0 +appVersion: "2.0.0" maintainers: - name: HellstromIT diff --git a/charts/patchmon/templates/NOTES.txt b/charts/patchmon/templates/NOTES.txt index 571dfb2..ff9aae0 100644 --- a/charts/patchmon/templates/NOTES.txt +++ b/charts/patchmon/templates/NOTES.txt @@ -1,10 +1,7 @@ -PatchMon installed. +PatchMon v2 installed. -Frontend: - Service: {{ include "patchmon.fullname" . }}-frontend:{{ .Values.service.frontend.port }} - -Backend: - Service: {{ include "patchmon.fullname" . }}-backend:{{ .Values.service.backend.port }} +Server: + Service: {{ include "patchmon.fullname" . }}-server:{{ .Values.service.port }} Exposure: {{- if .Values.gatewayAPI.enabled }} @@ -13,6 +10,11 @@ Exposure: {{- if .Values.ingress.enabled }} Ingress: {{ include "patchmon.fullname" . }} {{- end }} +{{- if not (or .Values.gatewayAPI.enabled .Values.ingress.enabled) }} + None configured — access via port-forward: + kubectl port-forward svc/{{ include "patchmon.fullname" . }}-server {{ .Values.service.port }}:{{ .Values.service.port }} +{{- end }} Database mode: {{ .Values.database.mode }} -Valkey subchart enabled: {{ .Values.valkey.enabled }} +Valkey enabled: {{ .Values.valkey.enabled }} +Guacd sidecar: {{ .Values.guacd.enabled }} diff --git a/charts/patchmon/templates/_helpers.tpl b/charts/patchmon/templates/_helpers.tpl index 37edf82..b88f85d 100644 --- a/charts/patchmon/templates/_helpers.tpl +++ b/charts/patchmon/templates/_helpers.tpl @@ -32,6 +32,13 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{- end }} +{{/* Validate that only one exposure method is enabled */}} +{{- define "patchmon.exposure.validate" -}} +{{- if and .Values.gatewayAPI.enabled .Values.ingress.enabled -}} + {{- fail "Only one exposure method can be enabled: set either gatewayAPI.enabled or ingress.enabled, not both" -}} +{{- end -}} +{{- end -}} + {{- define "patchmon.db.external.validate" -}} {{- if eq .Values.database.mode "external" -}} {{- $hasUriVal := and .Values.external.postgres.uri (ne .Values.external.postgres.uri "") -}} @@ -50,8 +57,8 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{- define "patchmon.redis.host" -}} -{{- if .Values.redis.host -}} -{{ .Values.redis.host }} +{{- if and .Values.patchmon.redis.host (ne .Values.patchmon.redis.host "") -}} +{{ .Values.patchmon.redis.host }} {{- else if .Values.valkey.enabled -}} {{ printf "%s-valkey" .Release.Name }} {{- else -}} @@ -59,6 +66,14 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{- end -}} +{{- define "patchmon.redis.port" -}} +{{- if and .Values.patchmon.redis.port (ne (.Values.patchmon.redis.port | toString) "") -}} +{{ .Values.patchmon.redis.port }} +{{- else -}} +{{ .Values.valkey.service.port }} +{{- end -}} +{{- end -}} + {{- define "patchmon.postgres.passwordSecretName" -}} {{- if .Values.postgres.auth.existingSecret -}} {{ .Values.postgres.auth.existingSecret }} @@ -71,7 +86,6 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- if .Values.postgres.auth.existingSecret -}} {{ default "password" .Values.postgres.auth.existingSecretPasswordKey }} {{- else -}} -{{- /* Always use the chart secret key name */ -}} POSTGRES_PASSWORD {{- end -}} {{- end -}} diff --git a/charts/patchmon/templates/configmap-backend.yaml b/charts/patchmon/templates/configmap-backend.yaml deleted file mode 100644 index 2a877e5..0000000 --- a/charts/patchmon/templates/configmap-backend.yaml +++ /dev/null @@ -1,65 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "patchmon.fullname" . }}-backend - labels: - {{- include "patchmon.labels" . | nindent 4 }} -data: - LOG_LEVEL: {{ .Values.patchmon.logLevel | quote }} - - SERVER_PROTOCOL: {{ .Values.patchmon.server.protocol | quote }} - SERVER_HOST: {{ .Values.patchmon.server.host | quote }} - SERVER_PORT: {{ .Values.patchmon.server.port | quote }} - CORS_ORIGIN: {{ .Values.patchmon.server.corsOrigin | quote }} - TRUST_PROXY: {{ .Values.patchmon.server.trustProxy | quote }} - ENABLE_HSTS: {{ .Values.patchmon.server.enableHsts | quote }} - - JWT_EXPIRES_IN: {{ .Values.patchmon.jwt.expiresIn | quote }} - JWT_REFRESH_EXPIRES_IN: {{ .Values.patchmon.jwt.refreshExpiresIn | quote }} - - DB_CONNECTION_LIMIT: {{ .Values.patchmon.dbPool.connectionLimit | quote }} - DB_POOL_TIMEOUT: {{ .Values.patchmon.dbPool.poolTimeout | quote }} - DB_CONNECT_TIMEOUT: {{ .Values.patchmon.dbPool.connectTimeout | quote }} - DB_IDLE_TIMEOUT: {{ .Values.patchmon.dbPool.idleTimeout | quote }} - DB_MAX_LIFETIME: {{ .Values.patchmon.dbPool.maxLifetime | quote }} - - RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.windowMs | quote }} - RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.max | quote }} - AUTH_RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.authWindowMs | quote }} - AUTH_RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.authMax | quote }} - AGENT_RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.agentWindowMs | quote }} - AGENT_RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.agentMax | quote }} - - TZ: {{ .Values.patchmon.timezone | quote }} - - {{- if .Values.patchmon.oidc.enabled }} - OIDC_ENABLED: "true" - OIDC_ISSUER_URL: {{ .Values.patchmon.oidc.issuerUrl | quote }} - OIDC_CLIENT_ID: {{ .Values.patchmon.oidc.clientId | quote }} - OIDC_REDIRECT_URI: {{ .Values.patchmon.oidc.redirectUri | quote }} - OIDC_SCOPES: {{ .Values.patchmon.oidc.scopes | quote }} - OIDC_AUTO_CREATE_USERS: {{ ternary "true" "false" .Values.patchmon.oidc.autoCreateUsers | quote }} - OIDC_DEFAULT_ROLE: {{ .Values.patchmon.oidc.defaultRole | quote }} - OIDC_BUTTON_TEXT: {{ .Values.patchmon.oidc.buttonText | quote }} - - # From docs - OIDC_DISABLE_LOCAL_AUTH: {{ ternary "true" "false" .Values.patchmon.oidc.disableLocalAuth | quote }} - OIDC_SYNC_ROLES: {{ ternary "true" "false" .Values.patchmon.oidc.syncRoles | quote }} - - {{- if .Values.patchmon.oidc.adminGroup }} - OIDC_ADMIN_GROUP: {{ .Values.patchmon.oidc.adminGroup | quote }} - {{- end }} - {{- if .Values.patchmon.oidc.userGroup }} - OIDC_USER_GROUP: {{ .Values.patchmon.oidc.userGroup | quote }} - {{- end }} - {{- if .Values.patchmon.oidc.superAdminGroup }} - OIDC_SUPERADMIN_GROUP: {{ .Values.patchmon.oidc.superAdminGroup | quote }} - {{- end }} - {{- if .Values.patchmon.oidc.hostManagerGroup }} - OIDC_HOST_MANAGER_GROUP: {{ .Values.patchmon.oidc.hostManagerGroup | quote }} - {{- end }} - {{- if .Values.patchmon.oidc.readOnlyGroup }} - OIDC_READONLY_GROUP: {{ .Values.patchmon.oidc.readOnlyGroup | quote }} - {{- end }} - {{- end }} - diff --git a/charts/patchmon/templates/configmap-server.yaml b/charts/patchmon/templates/configmap-server.yaml new file mode 100644 index 0000000..201d487 --- /dev/null +++ b/charts/patchmon/templates/configmap-server.yaml @@ -0,0 +1,100 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "patchmon.fullname" . }}-server + labels: + {{- include "patchmon.labels" . | nindent 4 }} +data: + LOG_LEVEL: {{ .Values.patchmon.logLevel | quote }} + CORS_ORIGIN: {{ .Values.patchmon.server.corsOrigin | quote }} + TRUST_PROXY: {{ .Values.patchmon.server.trustProxy | quote }} + ENABLE_HSTS: {{ .Values.patchmon.server.enableHsts | quote }} + {{- if .Values.patchmon.server.port }} + PORT: {{ .Values.patchmon.server.port | quote }} + {{- end }} + + JWT_EXPIRES_IN: {{ .Values.patchmon.jwt.expiresIn | quote }} + + # Auth / lockout + MAX_LOGIN_ATTEMPTS: {{ .Values.patchmon.auth.maxLoginAttempts | quote }} + LOCKOUT_DURATION_MINUTES: {{ .Values.patchmon.auth.lockoutDurationMinutes | quote }} + SESSION_INACTIVITY_TIMEOUT_MINUTES: {{ .Values.patchmon.auth.sessionInactivityTimeoutMinutes | quote }} + AUTH_BROWSER_SESSION_COOKIES: {{ .Values.patchmon.auth.browserSessionCookies | quote }} + TFA_MAX_REMEMBER_SESSIONS: {{ .Values.patchmon.auth.tfaMaxRememberSessions | quote }} + MAX_TFA_ATTEMPTS: {{ .Values.patchmon.auth.maxTfaAttempts | quote }} + TFA_LOCKOUT_DURATION_MINUTES: {{ .Values.patchmon.auth.tfaLockoutDurationMinutes | quote }} + TFA_REMEMBER_ME_EXPIRES_IN: {{ .Values.patchmon.auth.tfaRememberMeExpiresIn | quote }} + + # Password policy + PASSWORD_MIN_LENGTH: {{ .Values.patchmon.passwordPolicy.minLength | quote }} + PASSWORD_REQUIRE_UPPERCASE: {{ .Values.patchmon.passwordPolicy.requireUppercase | quote }} + PASSWORD_REQUIRE_LOWERCASE: {{ .Values.patchmon.passwordPolicy.requireLowercase | quote }} + PASSWORD_REQUIRE_NUMBER: {{ .Values.patchmon.passwordPolicy.requireNumber | quote }} + PASSWORD_REQUIRE_SPECIAL: {{ .Values.patchmon.passwordPolicy.requireSpecial | quote }} + + # DB pool + DB_CONNECTION_LIMIT: {{ .Values.patchmon.dbPool.connectionLimit | quote }} + DB_CONNECT_TIMEOUT: {{ .Values.patchmon.dbPool.connectTimeout | quote }} + DB_TRANSACTION_LONG_TIMEOUT: {{ .Values.patchmon.dbPool.transactionLongTimeout | quote }} + PM_DB_CONN_MAX_ATTEMPTS: {{ .Values.patchmon.dbPool.connMaxAttempts | quote }} + PM_DB_CONN_WAIT_INTERVAL: {{ .Values.patchmon.dbPool.connWaitInterval | quote }} + + # Rate limits + RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.windowMs | quote }} + RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.max | quote }} + AUTH_RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.authWindowMs | quote }} + AUTH_RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.authMax | quote }} + AGENT_RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.agentWindowMs | quote }} + AGENT_RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.agentMax | quote }} + PASSWORD_RATE_LIMIT_WINDOW_MS: {{ .Values.patchmon.rateLimit.passwordWindowMs | quote }} + PASSWORD_RATE_LIMIT_MAX: {{ .Values.patchmon.rateLimit.passwordMax | quote }} + + # Body limits + JSON_BODY_LIMIT: {{ .Values.patchmon.bodyLimits.json | quote }} + AGENT_UPDATE_BODY_LIMIT: {{ .Values.patchmon.bodyLimits.agentUpdate | quote }} + + # Redis TLS + {{- if .Values.patchmon.redis.tls }} + REDIS_TLS: "true" + REDIS_TLS_VERIFY: {{ .Values.patchmon.redis.tlsVerify | quote }} + {{- if .Values.patchmon.redis.tlsCa }} + REDIS_TLS_CA: {{ .Values.patchmon.redis.tlsCa | quote }} + {{- end }} + {{- end }} + REDIS_CONNECT_TIMEOUT_MS: {{ .Values.patchmon.redis.connectTimeoutMs | quote }} + REDIS_COMMAND_TIMEOUT_MS: {{ .Values.patchmon.redis.commandTimeoutMs | quote }} + + TZ: {{ .Values.patchmon.timezone | quote }} + + {{- if .Values.patchmon.oidc.enabled }} + OIDC_ENABLED: "true" + OIDC_ISSUER_URL: {{ .Values.patchmon.oidc.issuerUrl | quote }} + OIDC_CLIENT_ID: {{ .Values.patchmon.oidc.clientId | quote }} + OIDC_REDIRECT_URI: {{ .Values.patchmon.oidc.redirectUri | quote }} + OIDC_SCOPES: {{ .Values.patchmon.oidc.scopes | quote }} + OIDC_AUTO_CREATE_USERS: {{ ternary "true" "false" .Values.patchmon.oidc.autoCreateUsers | quote }} + OIDC_DEFAULT_ROLE: {{ .Values.patchmon.oidc.defaultRole | quote }} + OIDC_BUTTON_TEXT: {{ .Values.patchmon.oidc.buttonText | quote }} + OIDC_DISABLE_LOCAL_AUTH: {{ ternary "true" "false" .Values.patchmon.oidc.disableLocalAuth | quote }} + OIDC_SYNC_ROLES: {{ ternary "true" "false" .Values.patchmon.oidc.syncRoles | quote }} + OIDC_ENFORCE_HTTPS: {{ ternary "true" "false" .Values.patchmon.oidc.enforceHttps | quote }} + OIDC_SESSION_TTL: {{ .Values.patchmon.oidc.sessionTtl | quote }} + {{- if .Values.patchmon.oidc.postLogoutUri }} + OIDC_POST_LOGOUT_URI: {{ .Values.patchmon.oidc.postLogoutUri | quote }} + {{- end }} + {{- if .Values.patchmon.oidc.adminGroup }} + OIDC_ADMIN_GROUP: {{ .Values.patchmon.oidc.adminGroup | quote }} + {{- end }} + {{- if .Values.patchmon.oidc.userGroup }} + OIDC_USER_GROUP: {{ .Values.patchmon.oidc.userGroup | quote }} + {{- end }} + {{- if .Values.patchmon.oidc.superAdminGroup }} + OIDC_SUPERADMIN_GROUP: {{ .Values.patchmon.oidc.superAdminGroup | quote }} + {{- end }} + {{- if .Values.patchmon.oidc.hostManagerGroup }} + OIDC_HOST_MANAGER_GROUP: {{ .Values.patchmon.oidc.hostManagerGroup | quote }} + {{- end }} + {{- if .Values.patchmon.oidc.readOnlyGroup }} + OIDC_READONLY_GROUP: {{ .Values.patchmon.oidc.readOnlyGroup | quote }} + {{- end }} + {{- end }} diff --git a/charts/patchmon/templates/deployment-frontend.yaml b/charts/patchmon/templates/deployment-frontend.yaml deleted file mode 100644 index d46e851..0000000 --- a/charts/patchmon/templates/deployment-frontend.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "patchmon.fullname" . }}-frontend - labels: - {{- include "patchmon.labels" . | nindent 4 }} - app.kubernetes.io/component: frontend -spec: - replicas: {{ .Values.replicaCount.frontend }} - selector: - matchLabels: - {{- include "patchmon.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: frontend - template: - metadata: - labels: - {{- include "patchmon.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: frontend - annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap-backend.yaml") . | sha256sum }} - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "patchmon.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: frontend - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.frontend.repository }}:{{ default .Chart.AppVersion .Values.image.frontend.tag }}" - imagePullPolicy: {{ .Values.image.frontend.pullPolicy }} - ports: - - name: http - containerPort: 3000 - protocol: TCP - livenessProbe: - {{- toYaml .Values.livenessProbe.frontend | nindent 12 }} - readinessProbe: - {{- toYaml .Values.readinessProbe.frontend | nindent 12 }} - env: - - name: BACKEND_HOST - value: {{ include "patchmon.fullname" . }}-backend - - name: BACKEND_PORT - value: {{ .Values.service.backend.port | quote }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.topologySpreadConstraints }} - topologySpreadConstraints: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/charts/patchmon/templates/deployment-backend.yaml b/charts/patchmon/templates/deployment-server.yaml similarity index 55% rename from charts/patchmon/templates/deployment-backend.yaml rename to charts/patchmon/templates/deployment-server.yaml index 325a9c9..290033d 100644 --- a/charts/patchmon/templates/deployment-backend.yaml +++ b/charts/patchmon/templates/deployment-server.yaml @@ -1,24 +1,25 @@ {{- include "patchmon.db.external.validate" . -}} +{{- include "patchmon.exposure.validate" . -}} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "patchmon.fullname" . }}-backend + name: {{ include "patchmon.fullname" . }}-server labels: {{- include "patchmon.labels" . | nindent 4 }} - app.kubernetes.io/component: backend + app.kubernetes.io/component: server spec: - replicas: {{ .Values.replicaCount.backend }} + replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "patchmon.selectorLabels" . | nindent 6 }} - app.kubernetes.io/component: backend + app.kubernetes.io/component: server template: metadata: labels: {{- include "patchmon.selectorLabels" . | nindent 8 }} - app.kubernetes.io/component: backend + app.kubernetes.io/component: server annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap-backend.yaml") . | sha256sum }} + checksum/config: {{ include (print $.Template.BasePath "/configmap-server.yaml") . | sha256sum }} checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} @@ -32,22 +33,22 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - - name: backend + - name: server securityContext: {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.backend.repository }}:{{ default .Chart.AppVersion .Values.image.backend.tag }}" - imagePullPolicy: {{ .Values.image.backend.pullPolicy }} + image: "{{ .Values.image.server.repository }}:{{ default .Chart.AppVersion .Values.image.server.tag }}" + imagePullPolicy: {{ .Values.image.server.pullPolicy }} ports: - name: http - containerPort: 3001 + containerPort: 3000 protocol: TCP livenessProbe: - {{- toYaml .Values.livenessProbe.backend | nindent 12 }} + {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: - {{- toYaml .Values.readinessProbe.backend | nindent 12 }} + {{- toYaml .Values.readinessProbe | nindent 12 }} envFrom: - configMapRef: - name: {{ include "patchmon.fullname" . }}-backend + name: {{ include "patchmon.fullname" . }}-server env: - name: JWT_SECRET @@ -56,6 +57,18 @@ spec: name: {{ include "patchmon.fullname" . }} key: JWT_SECRET + - name: SESSION_SECRET + valueFrom: + secretKeyRef: + name: {{ include "patchmon.fullname" . }} + key: SESSION_SECRET + + - name: AI_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ include "patchmon.fullname" . }} + key: AI_ENCRYPTION_KEY + {{- if .Values.patchmon.oidc.enabled }} - name: OIDC_CLIENT_SECRET valueFrom: @@ -70,20 +83,24 @@ spec: {{- end }} # ---------------- - # Database settings + # Database # ---------------- - {{- $hasUriVal := and .Values.external.postgres.uri (ne .Values.external.postgres.uri "") -}} {{- $hasUriSecret := and .Values.external.postgres.uriFromSecret.name (ne .Values.external.postgres.uriFromSecret.name "") -}} {{- $hasPwSecret := and .Values.external.postgres.passwordFromSecret.name (ne .Values.external.postgres.passwordFromSecret.name "") -}} {{- if eq .Values.database.mode "internal" }} + - name: POSTGRES_HOST + value: {{ include "patchmon.fullname" . }}-postgres + - name: POSTGRES_USER + value: {{ .Values.postgres.auth.username | quote }} + - name: POSTGRES_DB + value: {{ .Values.postgres.auth.database | quote }} - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: {{ include "patchmon.postgres.passwordSecretName" . | quote }} key: {{ include "patchmon.postgres.passwordSecretKey" . | quote }} - - name: DATABASE_URL value: "postgresql://{{ .Values.postgres.auth.username }}:$(POSTGRES_PASSWORD)@{{ include "patchmon.fullname" . }}-postgres:5432/{{ .Values.postgres.auth.database }}" {{- else }} @@ -111,22 +128,50 @@ spec: - name: DATABASE_URL value: "$(EXT_PG_URI)" {{- else }} + - name: POSTGRES_HOST + value: {{ .Values.external.postgres.host | quote }} + - name: POSTGRES_USER + value: {{ .Values.external.postgres.username | quote }} + - name: POSTGRES_DB + value: {{ .Values.external.postgres.database | quote }} + - name: POSTGRES_PASSWORD + {{- if .Values.external.postgres.password }} + value: {{ .Values.external.postgres.password | quote }} + {{- else }} + valueFrom: + secretKeyRef: + name: {{ .Values.external.postgres.passwordFromSecret.name | quote }} + key: {{ .Values.external.postgres.passwordFromSecret.key | quote }} + {{- end }} - name: DATABASE_URL - value: "postgresql://{{ .Values.external.postgres.username }}:{{- if .Values.external.postgres.password -}}{{ .Values.external.postgres.password }}{{- else -}}$(EXT_PG_PASSWORD){{- end -}}@{{ .Values.external.postgres.host }}:{{ .Values.external.postgres.port }}/{{ .Values.external.postgres.database }}{{- if .Values.external.postgres.sslMode -}}?sslmode={{ .Values.external.postgres.sslMode }}{{- end -}}" + value: "postgresql://{{ .Values.external.postgres.username }}:$(POSTGRES_PASSWORD)@{{ .Values.external.postgres.host }}:{{ .Values.external.postgres.port }}/{{ .Values.external.postgres.database }}{{- if .Values.external.postgres.sslMode -}}?sslmode={{ .Values.external.postgres.sslMode }}{{- end -}}" {{- end }} - {{- end }} - - # --------------- - # Valkey - # --------------- + # ---------------- + # Redis / Valkey + # ---------------- - name: REDIS_HOST - value: {{ default (printf "%s-valkey" .Release.Name) .Values.patchmon.redis.host | quote }} + value: {{ include "patchmon.redis.host" . | quote }} - name: REDIS_PORT - value: {{ .Values.valkey.service.port | quote }} + value: {{ include "patchmon.redis.port" . | quote }} - name: REDIS_DB value: {{ .Values.patchmon.redis.db | quote }} + {{- if .Values.valkey.auth.enabled }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "patchmon.fullname" . }} + key: REDIS_PASSWORD + {{- end }} + + # ---------------- + # Guacd + # ---------------- + {{- if .Values.guacd.enabled }} + - name: GUACD_ADDRESS + value: "localhost:4822" + {{- end }} volumeMounts: - name: agent-files @@ -134,6 +179,33 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.guacd.enabled }} + - name: guacd + image: {{ .Values.guacd.image | quote }} + imagePullPolicy: {{ .Values.guacd.pullPolicy }} + securityContext: + {{- toYaml .Values.guacd.securityContext | nindent 12 }} + ports: + - name: guacd + containerPort: 4822 + protocol: TCP + livenessProbe: + tcpSocket: + port: guacd + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: guacd + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + {{- toYaml .Values.guacd.resources | nindent 12 }} + volumeMounts: + - name: guacd-tmp + mountPath: /tmp + {{- end }} + volumes: - name: agent-files {{- if .Values.patchmon.persistence.agentFiles.enabled }} @@ -142,6 +214,12 @@ spec: {{- else }} emptyDir: {} {{- end }} + {{- if .Values.guacd.enabled }} + - name: guacd-tmp + emptyDir: + medium: Memory + sizeLimit: 64Mi + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -157,4 +235,4 @@ spec: {{- with .Values.topologySpreadConstraints }} topologySpreadConstraints: {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/charts/patchmon/templates/httproute.yaml b/charts/patchmon/templates/httproute.yaml index e44ea6b..8ed13aa 100644 --- a/charts/patchmon/templates/httproute.yaml +++ b/charts/patchmon/templates/httproute.yaml @@ -19,15 +19,6 @@ spec: hostnames: {{- toYaml .Values.gatewayAPI.hostnames | nindent 4 }} rules: - - matches: - - path: - type: PathPrefix - value: /api - backendRefs: - - group: "" - kind: Service - name: {{ include "patchmon.fullname" . }}-backend - port: {{ .Values.service.backend.port }} - matches: - path: type: PathPrefix @@ -35,6 +26,6 @@ spec: backendRefs: - group: "" kind: Service - name: {{ include "patchmon.fullname" . }}-frontend - port: {{ .Values.service.frontend.port }} + name: {{ include "patchmon.fullname" . }}-server + port: {{ .Values.service.port }} {{- end }} diff --git a/charts/patchmon/templates/ingress.yaml b/charts/patchmon/templates/ingress.yaml index bd4c2b2..51909e4 100644 --- a/charts/patchmon/templates/ingress.yaml +++ b/charts/patchmon/templates/ingress.yaml @@ -20,19 +20,12 @@ spec: - host: {{ .host | quote }} http: paths: - - path: /api - pathType: Prefix - backend: - service: - name: {{ include "patchmon.fullname" . }}-backend - port: - number: {{ .Values.service.backend.port }} - path: / pathType: Prefix backend: service: - name: {{ include "patchmon.fullname" . }}-frontend + name: {{ include "patchmon.fullname" $ }}-server port: - number: {{ .Values.service.frontend.port }} + number: {{ $.Values.service.port }} {{- end }} {{- end }} diff --git a/charts/patchmon/templates/pdb.yaml b/charts/patchmon/templates/pdb.yaml index 24611c8..77784ca 100644 --- a/charts/patchmon/templates/pdb.yaml +++ b/charts/patchmon/templates/pdb.yaml @@ -15,4 +15,5 @@ spec: selector: matchLabels: {{- include "patchmon.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server {{- end }} diff --git a/charts/patchmon/templates/secret.yaml b/charts/patchmon/templates/secret.yaml index 58b39fa..91b44d3 100644 --- a/charts/patchmon/templates/secret.yaml +++ b/charts/patchmon/templates/secret.yaml @@ -10,6 +10,21 @@ {{- $oldJwt = (index (index $s "data") "JWT_SECRET") | b64dec -}} {{- end -}} +{{- $oldSession := "" -}} +{{- if and $s (index $s "data") (index (index $s "data") "SESSION_SECRET") -}} +{{- $oldSession = (index (index $s "data") "SESSION_SECRET") | b64dec -}} +{{- end -}} + +{{- $oldAiKey := "" -}} +{{- if and $s (index $s "data") (index (index $s "data") "AI_ENCRYPTION_KEY") -}} +{{- $oldAiKey = (index (index $s "data") "AI_ENCRYPTION_KEY") | b64dec -}} +{{- end -}} + +{{- $oldRedis := "" -}} +{{- if and $s (index $s "data") (index (index $s "data") "REDIS_PASSWORD") -}} +{{- $oldRedis = (index (index $s "data") "REDIS_PASSWORD") | b64dec -}} +{{- end -}} + {{- $oldOidc := "" -}} {{- if and $s (index $s "data") (index (index $s "data") "OIDC_CLIENT_SECRET") -}} {{- $oldOidc = (index (index $s "data") "OIDC_CLIENT_SECRET") | b64dec -}} @@ -27,11 +42,11 @@ stringData: POSTGRES_PASSWORD: {{ default (randAlphaNum 32) $oldPg | quote }} {{- end }} JWT_SECRET: {{ default (randAlphaNum 64) (default $oldJwt .Values.patchmon.jwt.secret) | quote }} -{{- /* - Only store OIDC_CLIENT_SECRET in this chart Secret when: - - OIDC is enabled - - user is NOT using an existing secret reference -*/ -}} + SESSION_SECRET: {{ default (randAlphaNum 64) $oldSession | quote }} + AI_ENCRYPTION_KEY: {{ default (randAlphaNum 64) $oldAiKey | quote }} +{{- if .Values.valkey.auth.enabled }} + REDIS_PASSWORD: {{ default (randAlphaNum 32) $oldRedis | quote }} +{{- end }} {{- if and .Values.patchmon.oidc.enabled (not (and .Values.patchmon.oidc.clientSecretFromSecret.name (ne .Values.patchmon.oidc.clientSecretFromSecret.name ""))) }} OIDC_CLIENT_SECRET: {{ default $oldOidc .Values.patchmon.oidc.clientSecret | required "patchmon.oidc.clientSecret is required when oidc is enabled and clientSecretFromSecret.name is not set" | quote }} {{- end }} diff --git a/charts/patchmon/templates/service-backend.yaml b/charts/patchmon/templates/service-backend.yaml deleted file mode 100644 index 33a6d2c..0000000 --- a/charts/patchmon/templates/service-backend.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "patchmon.fullname" . }}-backend - labels: - {{- include "patchmon.labels" . | nindent 4 }} - app.kubernetes.io/component: backend -spec: - type: {{ .Values.service.backend.type }} - selector: - {{- include "patchmon.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: backend - ports: - - name: http - port: {{ .Values.service.backend.port }} - targetPort: 3001 diff --git a/charts/patchmon/templates/service-frontend.yaml b/charts/patchmon/templates/service-frontend.yaml deleted file mode 100644 index 4903bf7..0000000 --- a/charts/patchmon/templates/service-frontend.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "patchmon.fullname" . }}-frontend - labels: - {{- include "patchmon.labels" . | nindent 4 }} - app.kubernetes.io/component: frontend -spec: - type: {{ .Values.service.frontend.type }} - selector: - {{- include "patchmon.selectorLabels" . | nindent 4 }} - app.kubernetes.io/component: frontend - ports: - - name: http - port: {{ .Values.service.frontend.port }} - targetPort: 3000 diff --git a/charts/patchmon/templates/service-server.yaml b/charts/patchmon/templates/service-server.yaml new file mode 100644 index 0000000..ddef14c --- /dev/null +++ b/charts/patchmon/templates/service-server.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "patchmon.fullname" . }}-server + labels: + {{- include "patchmon.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + type: {{ .Values.service.type }} + selector: + {{- include "patchmon.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: server + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: 3000 diff --git a/charts/patchmon/values.yaml b/charts/patchmon/values.yaml index fc7e3cb..bdb571c 100644 --- a/charts/patchmon/values.yaml +++ b/charts/patchmon/values.yaml @@ -1,28 +1,19 @@ image: - backend: - repository: ghcr.io/patchmon/patchmon-backend - # tag: latest - pullPolicy: IfNotPresent - frontend: - repository: ghcr.io/patchmon/patchmon-frontend - # tag: latest + server: + repository: ghcr.io/patchmon/patchmon-server + # tag defaults to Chart.appVersion + # tag: "" pullPolicy: IfNotPresent -replicaCount: - backend: 1 - frontend: 1 +replicaCount: 1 imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: - # Specifies whether a service account should be created create: true - # Annotations to add to the service account annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template name: "" podAnnotations: {} @@ -39,14 +30,12 @@ securityContext: runAsUser: 1000 service: - backend: - type: ClusterIP - port: 3001 - frontend: - type: ClusterIP - port: 3000 - -# Choose ONE exposure method + type: ClusterIP + port: 3000 + +# ----------------------------------------------------------- +# Choose ONE exposure method (gatewayAPI OR ingress) +# ----------------------------------------------------------- gatewayAPI: enabled: true parentRefs: @@ -64,61 +53,70 @@ ingress: - host: patchmon.example.com tls: [] - +# ----------------------------------------------------------- +# PatchMon application settings +# ----------------------------------------------------------- patchmon: logLevel: info server: - protocol: https - host: patchmon.example.com - port: 443 corsOrigin: https://patchmon.example.com trustProxy: true enableHsts: true + # port: 3000 # override the listening port inside the container oidc: enabled: false - - # Keycloak example: - # issuerUrl: https://keycloak.example.com/realms/your-realm issuerUrl: "" - clientId: "patchmon" - - # prefer secretRef; value is allowed but discouraged clientSecret: "" clientSecretFromSecret: - name: "" # if set, chart will NOT create/store the secret + name: "" key: "client-secret" - redirectUri: "https://patchmon.example.com/api/v1/auth/oidc/callback" scopes: "openid email profile groups" - autoCreateUsers: true defaultRole: "user" - buttonText: "Login with Keycloak" - - # From your docs + buttonText: "Login with SSO" disableLocalAuth: false syncRoles: false - adminGroup: "" # e.g. "patchmon-admins" - userGroup: "" # e.g. "patchmon-users" - superAdminGroup: "" # e.g. "patchmon-superadmins" - hostManagerGroup: "" # e.g. "patchmon-hostmanagers" - readOnlyGroup: "" # e.g. "patchmon-readonly" + enforceHttps: true + sessionTtl: 600 + postLogoutUri: "" + adminGroup: "" + userGroup: "" + superAdminGroup: "" + hostManagerGroup: "" + readOnlyGroup: "" jwt: # If empty, a random value is generated and persisted in the Secret via lookup. secret: "" expiresIn: "1h" - refreshExpiresIn: "7d" + + auth: + maxLoginAttempts: 5 + lockoutDurationMinutes: 15 + sessionInactivityTimeoutMinutes: 30 + browserSessionCookies: true + tfaMaxRememberSessions: 5 + maxTfaAttempts: 5 + tfaLockoutDurationMinutes: 30 + tfaRememberMeExpiresIn: "30d" + + passwordPolicy: + minLength: 8 + requireUppercase: true + requireLowercase: true + requireNumber: true + requireSpecial: true dbPool: connectionLimit: 30 - poolTimeout: 20 connectTimeout: 10 - idleTimeout: 300 - maxLifetime: 1800 + transactionLongTimeout: 60000 + connMaxAttempts: 30 + connWaitInterval: 2 rateLimit: windowMs: 900000 @@ -127,6 +125,12 @@ patchmon: authMax: 500 agentWindowMs: 60000 agentMax: 1000 + passwordWindowMs: 900000 + passwordMax: 5 + + bodyLimits: + json: "5mb" + agentUpdate: "2mb" timezone: "UTC" @@ -139,26 +143,43 @@ patchmon: redis: host: "" + port: "" db: 0 + tls: false + tlsVerify: false + # tlsCa: "" + connectTimeoutMs: 60000 + commandTimeoutMs: 60000 + +# ----------------------------------------------------------- +# Guacd sidecar (Windows RDP support) +# ----------------------------------------------------------- +guacd: + enabled: true + image: guacamole/guacd:latest + pullPolicy: IfNotPresent + resources: + limits: + memory: 512Mi + cpu: "1.0" + securityContext: + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL -# ----------------------- +# ----------------------------------------------------------- # Database configuration -# ----------------------- +# ----------------------------------------------------------- database: mode: internal # internal | external postgres: - image: postgres:16-alpine + image: postgres:17-alpine storage: size: 10Gi # storageClassName: "" resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi securityContext: runAsUser: 70 runAsGroup: 70 @@ -176,31 +197,33 @@ postgres: external: postgres: - # If uri is set (direct or from secret), other values are ignored. uri: "" uriFromSecret: - name: "" # CNPG: "-app" + name: "" key: "uri" - - # Used only when uri isn't set host: "" port: 5432 database: "patchmon_db" username: "patchmon_user" - password: "" # discouraged; prefer passwordFromSecret + password: "" passwordFromSecret: name: "" key: "password" + sslMode: "" - # Optional if you are constructing (ignored if uri is used) - sslMode: "" # e.g. "require" - -# ----------------------- -# Valkey -# ----------------------- +# ----------------------------------------------------------- +# Valkey (Redis-compatible cache/queue) +# ----------------------------------------------------------- valkey: enabled: true + # To enable Valkey auth, set auth.enabled: true and configure aclUsers. + # You must also set auth.usersExistingSecret to the chart secret name + # (e.g. "-patchmon") with passwordKey: REDIS_PASSWORD, or + # provide an inline password. See the Valkey chart docs for details. + auth: + enabled: false + service: port: 6379 @@ -209,52 +232,24 @@ valkey: persistence: size: 1Gi -resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi +resources: {} livenessProbe: - backend: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - frontend: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 readinessProbe: - backend: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - frontend: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 pdb: create: false