diff --git a/docs/configuration/istio/ingress.md b/docs/configuration/istio/ingress.md index 364730afe..acf7e16c6 100644 --- a/docs/configuration/istio/ingress.md +++ b/docs/configuration/istio/ingress.md @@ -88,3 +88,7 @@ variables: tenant_tls_cert: # base64 encoded tenant cert here tenant_tls_key: # base64 encoded tenant key here ``` + +{{% alert-note %}} +If you are using Private PKI or self-signed certificates for your tenant certificates it is necessary to additionally configure `UDS_CA_CERT` with additional [trusted certificate authorities](https://uds.defenseunicorns.com/core/configuration/uds-operator/#trusted-certificate-authority). +{{% /alert-note %}} diff --git a/docs/configuration/uds-monitoring-metrics.md b/docs/configuration/uds-monitoring-metrics.md index 5274d3225..bad32a6fa 100644 --- a/docs/configuration/uds-monitoring-metrics.md +++ b/docs/configuration/uds-monitoring-metrics.md @@ -4,10 +4,12 @@ type: docs weight: 1 --- -UDS Core leverages Pepr to handle setup of Prometheus scraping metrics endpoints, with the particular configuration necessary to work in a STRICT mTLS (Istio) environment. We handle this with both mutations of existing service monitors and generation of service monitors via the `Package` CR. +UDS Core leverages Pepr to handle setup of Prometheus scraping metrics endpoints, with the particular configuration necessary to work in a STRICT mTLS (Istio) environment. We handle this via a default scrapeClass in prometheus to add the istio certs. When a monitor needs to be exempt from that tlsConfig a mutation is performed to leverage a plain scrape class without istio certs. ## Mutations +Note: The below implementation has been deprecated in favor of a default `scrapeClass` with the file-based `tlsConfig` required for istio mTLS in prometheus automatically, supplemented with a mutation of `scrapeClass: exempt` that exempts monitors from the `tlsConfig` required for istio if the destination namespace is not istio injected (e.g. kube-system), unless the `uds/skip-sm-mutate` annotation is specified. The mutation behavior stated in the paragraph immediately below this section will be removed in a later release. + All service monitors are mutated to set the scrape scheme to HTTPS and set the TLS Config to what is required for Istio mTLS scraping (see [this doc](https://istio.io/latest/docs/ops/integrations/prometheus/#tls-settings) for details). Beyond this, no other fields are mutated. Supporting existing service monitors is useful since some charts include service monitors by default with more advanced configurations, and it is in our best interest to enable those and use them where possible. Assumptions are made about STRICT mTLS here for simplicity, based on the `istio-injection` namespace label. Without making these assumptions we would need to query `PeerAuthentication` resources or another resource to determine the exact workload mTLS posture. @@ -16,7 +18,7 @@ Note: This mutation is the default behavior for all service monitors but can be ## Package CR `monitor` field -UDS Core also supports generating service monitors from the `monitor` list in the `Package` spec. Charts do not always support service monitors, so generating them can be useful. This also provides a simplified way for other users to create service monitors, similar to the way we handle `VirtualServices` today. A full example of this can be seen below: +UDS Core also supports generating `ServiceMonitors` and/or `PodMonitors` from the `monitor` list in the `Package` spec. Charts do not always support monitors, so generating them can be useful. This also provides a simplified way for other users to create monitors, similar to the way we handle `VirtualServices` today. A full example of this can be seen below: ```yaml ... @@ -28,9 +30,16 @@ spec: targetPort: 1234 # Corresponding target port on the pod/container (for network policy) # Optional properties depending on your application description: "Metrics" # Add to customize the service monitor name + kind: ServiceMonitor # optional, kind defaults to service monitor if not specified. PodMonitor is the other valid option. podSelector: # Add if pod labels are different than `selector` (for network policy) app: barfoo path: "/mymetrics" # Add if metrics are exposed on a different path than "/metrics" + authorization: # Add if authorization is required for the metrics endpoint + credentials: + key: "example-key" + name: "example-secret" + optional: false + type: "Bearer" ``` This config is used to generate service monitors and corresponding network policies to setup scraping for your applications. The `ServiceMonitor`s will go through the mutation process to add `tlsConfig` and `scheme` to work in an istio environment. diff --git a/docs/configuration/uds-operator.md b/docs/configuration/uds-operator.md index b8319ffb0..87d340d70 100644 --- a/docs/configuration/uds-operator.md +++ b/docs/configuration/uds-operator.md @@ -21,9 +21,11 @@ The UDS Operator plays a pivotal role in managing the lifecycle of UDS Package C - **SSO Group Authentication:** - Group authentication determines who can access the application based on keycloak group membership. - At this time `anyOf` allows defining a list of groups, a user must belong to at least one of them. - {{% alert-caution %}} - Warning: **SSO Group Authentication** is in Alpha and may not be stable. Avoid using in production. Feedback is appreciated to improve reliability. - {{% /alert-caution %}} +- **Authservice Protection:** + - Authservice authentication provides application agnostic SSO for applications that opt-in. + {{% alert-caution %}} + Warning: **Authservice Protection** and **SSO Group Authentication** are in Alpha and may not be stable. Avoid using in production. Feedback is appreciated to improve reliability. + {{% /alert-caution %}} ### Example UDS Package CR @@ -70,51 +72,6 @@ spec: - /UDS Core/Admin ``` -## Exemption - -- **Exemption Scope:** - - Granting exemption for custom resources is restricted to the `uds-policy-exemptions` namespace by default, unless specifically configured to allow exemptions across all namespaces. -- **Policy Updates:** - - Updating the policies Pepr store with registered exemptions. - -### Example UDS Exemption CR - -```yaml -apiVersion: uds.dev/v1alpha1 -kind: Exemption -metadata: - name: neuvector - namespace: uds-policy-exemptions -spec: - exemptions: - - policies: - - DisallowHostNamespaces - - DisallowPrivileged - - RequireNonRootUser - - DropAllCapabilities - - RestrictHostPathWrite - - RestrictVolumeTypes - matcher: - namespace: neuvector - name: "^neuvector-enforcer-pod.*" - - - policies: - - DisallowPrivileged - - RequireNonRootUser - - DropAllCapabilities - - RestrictHostPathWrite - - RestrictVolumeTypes - matcher: - namespace: neuvector - name: "^neuvector-controller-pod.*" - - - policies: - - DropAllCapabilities - matcher: - namespace: neuvector - name: "^neuvector-prometheus-exporter-pod.*" -``` - ### Example UDS Package CR with SSO Templating By default, UDS generates a secret for the Single Sign-On (SSO) client that encapsulates all client contents as an opaque secret. In this setup, each key within the secret corresponds to its own environment variable or file, based on the method used to mount the secret. If customization of the secret rendering is required, basic templating can be achieved using the `secretTemplate` property. Below are examples showing this functionality. To see how templating works, please see the [Regex website](https://regex101.com/r/e41Dsk/3). @@ -164,6 +121,98 @@ spec: bearer_only: clientField(bearerOnly) ``` +### Protecting a UDS Package with Authservice + +To enable authentication for applications that do not have native OIDC configuration, UDS Core can utilize Authservice as an authentication layer. + +Follow these steps to protect your application with Authservice: + +* Set `enableAuthserviceSelector` with a matching label selector in the `sso` configuration of the Package. +* Ensure that the pods of the application are labeled with the corresponding selector + +```yaml +apiVersion: uds.dev/v1alpha1 +kind: Package +metadata: + name: httpbin + namespace: httpbin +spec: + sso: + - name: Demo SSO httpbin + clientId: uds-core-httpbin + redirectUris: + - "https://httpbin.uds.dev/login" + enableAuthserviceSelector: + app: httpbin +``` + +{{% alert-note %}} +The UDS Operator uses the first `redirectUris` to populate the `match.prefix` hostname and `callback_uri` in the authservice chain. +{{% /alert-note %}} + +For a complete example, see [app-authservice-tenant.yaml](https://github.com/defenseunicorns/uds-core/blob/main/src/test/app-authservice-tenant.yaml) + +#### Trusted Certificate Authority + +Authservice can be configured with additional trusted certificate bundle in cases where UDS Core ingress gateways are deployed with private PKI. + +To configure, set [UDS_CA_CERT](https://github.com/defenseunicorns/uds-core/blob/main/packages/standard/zarf.yaml#L11-L13) as an environment variable with a Base64 encoded PEM formatted certificate bundle that can be used to verify the certificates of the tenant gateway. + +Alternatively you can specify the `CA_CERT` variable in your `uds-config.yaml`: + +```yaml +variables: + core: + CA_CERT: +``` + +See [configuring Istio Ingress](https://uds.defenseunicorns.com/core/configuration/istio/ingress/#configure-domain-name-and-tls-for-istio-gateways) for the relevant documentation on configuring ingress certificates. + +## Exemption + +- **Exemption Scope:** + - Granting exemption for custom resources is restricted to the `uds-policy-exemptions` namespace by default, unless specifically configured to allow exemptions across all namespaces. +- **Policy Updates:** + - Updating the policies Pepr store with registered exemptions. + +### Example UDS Exemption CR + +```yaml +apiVersion: uds.dev/v1alpha1 +kind: Exemption +metadata: + name: neuvector + namespace: uds-policy-exemptions +spec: + exemptions: + - policies: + - DisallowHostNamespaces + - DisallowPrivileged + - RequireNonRootUser + - DropAllCapabilities + - RestrictHostPathWrite + - RestrictVolumeTypes + matcher: + namespace: neuvector + name: "^neuvector-enforcer-pod.*" + + - policies: + - DisallowPrivileged + - RequireNonRootUser + - DropAllCapabilities + - RestrictHostPathWrite + - RestrictVolumeTypes + matcher: + namespace: neuvector + name: "^neuvector-controller-pod.*" + + - policies: + - DropAllCapabilities + matcher: + namespace: neuvector + name: "^neuvector-prometheus-exporter-pod.*" +``` + ### Configuring UDS Core Policy Exemptions Default [policy exemptions](https://github.com/defenseunicorns/uds-core/blob/main/src/pepr/operator/crd/generated/exemption-v1alpha1.ts) are confined to a singular namespace: `uds-policy-exemptions`. We find this to be an optimal approach for UDS due to the following reasons: diff --git a/package-lock.json b/package-lock.json index c318fba67..60162a983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6658,7 +6658,6 @@ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.0.tgz", "integrity": "sha512-eFmkE9MG0+oT6nqSOcUwL+2UUmK2IvhhUV8hFDsCHnc++v2WCCbQQZh5vvjsa8sgOY/g9T0325hmkEmi6rninA==", "dev": true, - "license": "MIT", "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", diff --git a/package.json b/package.json index 5b8ed29f5..1b7e6beaf 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "env": { "UDS_DOMAIN": "###ZARF_VAR_DOMAIN###", + "UDS_CA_CERT": "###ZARF_VAR_CA_CERT###", "UDS_ALLOW_ALL_NS_EXEMPTIONS": "###ZARF_VAR_ALLOW_ALL_NS_EXEMPTIONS###", "UDS_SINGLE_TEST": "###ZARF_VAR_UDS_SINGLE_TEST###", "UDS_LOG_LEVEL": "###ZARF_VAR_UDS_LOG_LEVEL###" diff --git a/packages/slim-dev/zarf.yaml b/packages/slim-dev/zarf.yaml index f069e66ec..c7f0e40a4 100644 --- a/packages/slim-dev/zarf.yaml +++ b/packages/slim-dev/zarf.yaml @@ -7,6 +7,11 @@ metadata: version: "0.23.0" # x-release-please-end +variables: + - name: CA_CERT + description: "Base64 encoded CA cert that signed the domain wildcard certs used for Istio ingress" + default: "" + components: # CRDs - name: prometheus-operator-crds diff --git a/packages/standard/zarf.yaml b/packages/standard/zarf.yaml index 4ea3e6ac2..196275fd3 100644 --- a/packages/standard/zarf.yaml +++ b/packages/standard/zarf.yaml @@ -7,6 +7,11 @@ metadata: version: "0.23.0" # x-release-please-end +variables: + - name: CA_CERT + description: "Base64 encoded CA cert that signed the domain wildcard certs used for Istio ingress" + default: "" + components: # CRDs - name: prometheus-operator-crds diff --git a/pepr.ts b/pepr.ts index 98c06ecef..885b09d3d 100644 --- a/pepr.ts +++ b/pepr.ts @@ -1,20 +1,25 @@ -import { Log, PeprModule } from "pepr"; +import { PeprModule } from "pepr"; import cfg from "./package.json"; import { DataStore } from "pepr/dist/lib/storage"; import { istio } from "./src/pepr/istio"; +import { Component, setupLogger } from "./src/pepr/logger"; import { operator } from "./src/pepr/operator"; +import { setupAuthserviceSecret } from "./src/pepr/operator/controllers/keycloak/authservice/config"; import { Policy } from "./src/pepr/operator/crd"; import { registerCRDs } from "./src/pepr/operator/crd/register"; import { policies, startExemptionWatch } from "./src/pepr/policies"; import { prometheus } from "./src/pepr/prometheus"; +const log = setupLogger(Component.STARTUP); + (async () => { // Apply the CRDs to the cluster await registerCRDs(); // KFC watch for exemptions and update in-memory map await startExemptionWatch(); + await setupAuthserviceSecret(); new PeprModule(cfg, [ // UDS Core Operator operator, @@ -33,19 +38,19 @@ import { prometheus } from "./src/pepr/prometheus"; process.env.PEPR_MODE === "dev" || (process.env.PEPR_WATCH_MODE === "true" && cfg.version === "0.5.0") ) { - Log.debug("Clearing legacy pepr store exemption entries..."); + log.debug("Clearing legacy pepr store exemption entries..."); policies.Store.onReady((data: DataStore) => { const policiesList = Object.values(Policy); for (const p of Object.keys(data)) { // if p matches a Policy key, remove it if (policiesList.includes(p as Policy)) { - Log.debug(`Removing legacy storage of ${p} policy exemptions...`); + log.debug(`Removing legacy storage of ${p} policy exemptions...`); policies.Store.removeItem(p); } } }); } })().catch(err => { - Log.error(err); + log.error(err, "Critical error during startup. Exiting..."); process.exit(1); }); diff --git a/renovate.json b/renovate.json index 2b17d5ced..c03970695 100644 --- a/renovate.json +++ b/renovate.json @@ -38,6 +38,11 @@ } ], "packageRules": [ + { + "matchFileNames": ["src/authservice/**"], + "groupName": "authservice", + "commitMessageTopic": "authservice" + }, { "matchFileNames": ["src/istio/**"], "groupName": "istio", diff --git a/src/authservice/README.md b/src/authservice/README.md new file mode 100644 index 000000000..8f3c5459b --- /dev/null +++ b/src/authservice/README.md @@ -0,0 +1,7 @@ +## Authservice +`authservice` helps delegate the [OIDC Authorization Code Grant Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) +to the Istio mesh. `authservice` is compatible with any standard OIDC Provider as well as other Istio End-user Auth features, +including [Authentication Policy](https://istio.io/docs/tasks/security/authn-policy/) and [RBAC](https://istio.io/docs/tasks/security/rbac-groups/). +Together, they allow developers to protect their APIs and web apps without any application code required. + +See [IDAM.md](../../docs/IDAM.md) for guidance on using the [UDS Package](../pepr/operator/README.md) custom resource to generate Authservice chains. diff --git a/src/authservice/chart/Chart.yaml b/src/authservice/chart/Chart.yaml index b66be7037..93ca95965 100644 --- a/src/authservice/chart/Chart.yaml +++ b/src/authservice/chart/Chart.yaml @@ -15,9 +15,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.5.3 +version: 1.0.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 0.5.3 +appVersion: 1.0.1 diff --git a/src/authservice/chart/templates/authn.yaml b/src/authservice/chart/templates/authn.yaml deleted file mode 100644 index 1c16c105c..000000000 --- a/src/authservice/chart/templates/authn.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Authservice is non-functional without Istio/RequestAuthentication but we wrap this in a conditional to handle standalone testing -{{- if .Capabilities.APIVersions.Has "security.istio.io/v1beta1" }} -apiVersion: security.istio.io/v1beta1 -kind: RequestAuthentication -metadata: - name: jwt-authn - namespace: istio-system -spec: - selector: - matchLabels: - {{ .Values.selector.key }}: {{ .Values.selector.value | quote }} - jwtRules: - - issuer: https://{{ .Values.global.oidc.host }}/auth/realms/{{ .Values.global.oidc.realm }} - {{- if .Values.global.jwks }} - jwks: {{ .Values.global.jwks | quote }} - {{- else }} - jwksUri: https://{{ .Values.global.oidc.host }}/auth/realms/{{ .Values.global.oidc.realm }}/protocol/openid-connect/certs - {{- end }} - forwardOriginalToken: true -{{- end }} diff --git a/src/authservice/chart/templates/authz.yaml b/src/authservice/chart/templates/authz.yaml deleted file mode 100644 index c428885f2..000000000 --- a/src/authservice/chart/templates/authz.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Authservice is non-functional without Istio/AuthorizationPolicy but we wrap this in a conditional to handle standalone testing -{{- if .Capabilities.APIVersions.Has "security.istio.io/v1beta1" }} -apiVersion: security.istio.io/v1beta1 -kind: AuthorizationPolicy -metadata: - name: authservice - namespace: istio-system -spec: - selector: - matchLabels: - {{ .Values.selector.key }}: {{ .Values.selector.value | quote }} - action: CUSTOM - provider: - name: authservice - rules: - {{- if .Values.allow_unmatched_requests }} - - {} - {{- else if .Values.custom_authpolicy_rules }} -{{ .Values.custom_authpolicy_rules | toYaml | indent 2 }} - {{- else }} - - to: - - operation: - hosts: - - "*.{{ .Values.domain }}" - {{- end }} ---- -apiVersion: security.istio.io/v1beta1 -kind: AuthorizationPolicy -metadata: - name: jwt-authz - namespace: istio-system -spec: - selector: - matchLabels: - {{ .Values.selector.key }}: {{ .Values.selector.value | quote }} - rules: - - from: - - source: - requestPrincipals: - - "https://{{ .Values.global.oidc.host }}/auth/realms/{{ .Values.global.oidc.realm }}/*" -{{- end }} diff --git a/src/authservice/chart/templates/deployment.yaml b/src/authservice/chart/templates/deployment.yaml index 2729985d8..4dd4295f8 100644 --- a/src/authservice/chart/templates/deployment.yaml +++ b/src/authservice/chart/templates/deployment.yaml @@ -15,10 +15,11 @@ spec: template: metadata: annotations: - checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - {{- with .Values.podAnnotations }} + # Pre-create an empty checksum to ensure pod cycles when first update occurs + pepr.dev/checksum: "initialized" + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} labels: {{- include "authservice.selectorLabels" . | nindent 8 }} spec: @@ -30,11 +31,6 @@ spec: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- if .Values.global.certificate_authority }} - env: - - name: SSL_CERT_FILE - value: /mnt/ca-bundle/ca-bundle.crt - {{- end}} ports: - name: http containerPort: 10003 @@ -50,29 +46,6 @@ spec: volumeMounts: - name: {{ include "authservice.name" . }} mountPath: /etc/authservice - {{- if .Values.global.certificate_authority }} - - name: ca-bundle - mountPath: /mnt/ca-bundle - {{- end }} - {{- if .Values.global.certificate_authority }} - initContainers: - - name: update-ca-bundle - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - command: - - sh - - -c - - | - cat /etc/pki/tls/certs/* > /mnt/ca-bundle/ca-bundle.crt - volumeMounts: - - name: sso-tls-ca - mountPath: /etc/pki/tls/certs/oidc-ca.crt - subPath: oidc-ca.crt - readOnly: true - - name: ca-bundle - mountPath: /mnt/ca-bundle - {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -88,12 +61,4 @@ spec: volumes: - name: {{ include "authservice.name" . }} secret: - secretName: {{ include "authservice.fullname" . }} - {{- if .Values.global.certificate_authority }} - - name: sso-tls-ca - secret: - secretName: {{ include "authservice.fullname" . }}-sso-tls-ca - - name: ca-bundle - emptyDir: - sizeLimit: 5Mi - {{- end}} + secretName: {{ include "authservice.fullname" . }}-uds diff --git a/src/authservice/chart/templates/secret-ca.yaml b/src/authservice/chart/templates/secret-ca.yaml deleted file mode 100644 index c62a57470..000000000 --- a/src/authservice/chart/templates/secret-ca.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if .Values.global.certificate_authority }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "authservice.fullname" . }}-sso-tls-ca - namespace: {{ .Release.Namespace }} - labels: - {{- include "authservice.labels" . | nindent 4 }} -stringData: - oidc-ca.crt: {{ .Values.global.certificate_authority | quote }} -{{- end }} \ No newline at end of file diff --git a/src/authservice/chart/templates/secret.yaml b/src/authservice/chart/templates/secret.yaml deleted file mode 100644 index 8df1a07bb..000000000 --- a/src/authservice/chart/templates/secret.yaml +++ /dev/null @@ -1,139 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "authservice.fullname" . }} - namespace: {{ .Release.Namespace }} - labels: - {{- include "authservice.labels" . | nindent 4 }} -stringData: - config.json: | - { - "allow_unmatched_requests": {{ .Values.allow_unmatched_requests }}, - "listen_address": "0.0.0.0", - "listen_port": "10003", - {{- if .Values.trigger_rules }} - "trigger_rules": {{ toJson .Values.trigger_rules }}, - {{- end }} - "log_level": "{{ .Values.config.logLevel }}", - "default_oidc_config": { - "skip_verify_peer_cert": {{ $.Values.global.skip_verify_peer_cert }}, - "authorization_uri": "https://{{ $.Values.global.oidc.host }}/auth/realms/{{ $.Values.global.oidc.realm }}/protocol/openid-connect/auth", - "token_uri": "https://{{ $.Values.global.oidc.host }}/auth/realms/{{ $.Values.global.oidc.realm }}/protocol/openid-connect/token", - {{- if $.Values.global.jwks }} - "jwks": {{ $.Values.global.jwks | quote }}, - {{- else }} - "jwks_fetcher": { - "jwks_uri": "https://{{ $.Values.global.oidc.host }}/auth/realms/{{ $.Values.global.oidc.realm }}/protocol/openid-connect/certs", - "periodic_fetch_interval_sec": {{ $.Values.global.periodic_fetch_interval_sec }}, - "skip_verify_peer_cert": "{{ $.Values.global.skip_verify_peer_cert }}" - }, - {{- end }} - "client_id": "{{ $.Values.global.client_id }}", - "client_secret": "{{ $.Values.global.client_secret }}", - "id_token": { - "preamble": "Bearer", - "header": "Authorization" - }, - "access_token": { - "header": "JWT" - }, - {{- if contains "\\n" $.Values.global.certificate_authority }} - "trusted_certificate_authority": "{{ $.Values.global.certificate_authority }}", - {{- else }} - "trusted_certificate_authority": {{ $.Values.global.certificate_authority | quote }}, - {{- end }} - "logout": { - "path": "{{ $.Values.global.logout_path }}"{{ if $.Values.global.logout_redirect_uri }}, - "redirect_uri": "{{ $.Values.global.logout_redirect_uri }}" - {{- else if $.Values.global.oidc }}, - "redirect_uri": "https://{{ $.Values.global.oidc.host }}/auth/realms/{{ $.Values.global.oidc.realm}}/protocol/openid-connect/token/logout" - {{- end }} - }, - "absolute_session_timeout": "{{ $.Values.global.absolute_session_timeout }}", - "idle_session_timeout": "{{ $.Values.global.idle_session_timeout }}", - "scopes": [] - }, - "threads": 8, - "chains": [ - {{- range $k, $v := $.Values.chains }}{{ if ne $k ( first (keys $.Values.chains | sortAlpha) ) }},{{ end }} - { - "name": "{{ $k }}", - "match": { - {{- if .match }} - "header": "{{ .match.header | default $.Values.global.match.header }}", - {{- if .match.prefix }} - "prefix": "{{ tpl .match.prefix $ }}" - {{- else if .match.equality }} - "equality": "{{ .match.equality }}" - {{- else }} - "prefix": "{{ $.Values.global.match.prefix }}" - {{- end }} - {{- else }} - "header": "{{ $.Values.global.match.header }}", - "prefix": "{{ $.Values.global.match.prefix }}" - {{- end }} - }, - "filters": [ - { - "oidc_override": { - "authorization_uri": "https://{{ (dig "oidc" "host" $.Values.global.oidc.host .) }}/auth/realms/{{ (dig "oidc" "realm" $.Values.global.oidc.realm .) }}/protocol/openid-connect/auth", - "token_uri": "https://{{ (dig "oidc" "host" $.Values.global.oidc.host .) }}/auth/realms/{{ (dig "oidc" "realm" $.Values.global.oidc.realm .) }}/protocol/openid-connect/token", - {{- if or .redis_server_uri $.Values.global.redis_server_uri }} - "redis_session_store_config": { - "server_uri": {{ .redis_server_uri | default $.Values.global.redis_server_uri | quote }} - }, - {{- end }} - {{- if .callback_uri }} - "callback_uri": "{{ tpl .callback_uri $ | default $.Values.callback_uri }}", - {{- else }} - {{- fail "ERROR: Missing required field 'callback_uri' in one of the config chains" }} - {{ end }} - {{- if .jwks }} - "jwks": {{ .jwks | quote }}, - {{- else if .jwks_uri }} - "jwks_fetcher": { - "jwks_uri": {{ .jwks_uri | quote }}, - "periodic_fetch_interval_sec": {{ .periodic_fetch_interval_sec | default 60}}, - "skip_verify_peer_cert": {{ .skip_verify_peer_cert | default $.Values.global.skip_verify_peer_cert }} - }, - {{- end }} - {{- if .client_id }} - "client_id": "{{ .client_id }}", - {{- end }} - {{- if .client_secret }} - "client_secret": "{{ .client_secret }}", - {{- end }} - "cookie_name_prefix": "{{ default $k .cookie_name_prefix }}", - {{- if .certificate_authority }} - {{- if contains "\\n" .certificate_authority }} - "trusted_certificate_authority": "{{ .certificate_authority }}", - {{- else }} - "trusted_certificate_authority": {{ .certificate_authority | quote }}, - {{- end }} - {{- end }} - "logout": { - {{- if .logout_path }} - "path": "{{ .logout_path | default $.Values.global.logout_path }}", - {{- end }} - {{- if .logout_redirect_uri }} - "redirect_uri": "{{ .logout_redirect_uri | default $.Values.global.logout_redirect_uri }}" - {{- else if .oidc }} - "redirect_uri": "https://{{ .oidc.host | default $.Values.global.oidc.host }}/auth/realms/{{ .oidc.realm | default $.Values.global.oidc.realm}}/protocol/openid-connect/token/logout" - {{- else }} - "redirect_uri": "https://{{ $.Values.global.oidc.host }}/auth/realms/{{ $.Values.global.oidc.realm }}/protocol/openid-connect/token/logout" - {{- end}} - }, - {{- if .absolute_session_timeout }} - "absolute_session_timeout": "{{ .absolute_session_timeout }}", - {{- end }} - {{- if .idle_session_timeout }} - "idle_session_timeout": "{{ .idle_session_timeout }}", - {{- end }} - "scopes": {{ default list .scopes | toJson }} - } - } - ] - } - {{- end }} - ] - } diff --git a/src/authservice/chart/templates/uds-package.yaml b/src/authservice/chart/templates/uds-package.yaml index 3884987bb..ac12b65bd 100644 --- a/src/authservice/chart/templates/uds-package.yaml +++ b/src/authservice/chart/templates/uds-package.yaml @@ -22,7 +22,5 @@ spec: podLabels: app.kubernetes.io/name: authservice remoteNamespace: "" # Any namespace could have a protected app - remotePodLabels: - {{ .Values.selector.key }}: {{ .Values.selector.value }} port: 10003 description: "Protected Apps" diff --git a/src/authservice/chart/values.yaml b/src/authservice/chart/values.yaml index c1cd139c3..b28496153 100644 --- a/src/authservice/chart/values.yaml +++ b/src/authservice/chart/values.yaml @@ -7,91 +7,6 @@ image: # -- Overrides the image tag whose default is the chart appVersion. tag: "" -# -- If true will allow the requests even no filter chain match is found -allow_unmatched_requests: false - -# -- Extra Ruleset for AuthorizationPolicy CUSTOM action to forward to Authservice. -# To enable `allow_unmatched_requests` must be `false`. These custom rules mean that only these requests -# will be routed and will break default UDS Core setup for `prometheus/alertmanager/tempo` unless added. -# Path specific Operations are not supported, it is recommended to use only hosts, notHosts, & method operations. -# See reference: https://istio.io/latest/docs/reference/config/security/authorization-policy/ -custom_authpolicy_rules: - - when: - - key: request.headers[authorization] - notValues: - - "*" - -global: - # -- Default client_id to be used in each chain - client_id: "global_id" - # -- Default client_secret to be used in each chain - client_secret: "global_secret" - match: - # -- Header to match. The value ":authority" is used to match the requested hostname - header: ":authority" - # -- value matches the start of the header value defined above - prefix: "uds" - # -- Logout URL for the client - logout_path: "/globallogout" - # -- Logout Redirect URI for the client - logout_redirect_uri: "" - absolute_session_timeout: 0 - idle_session_timeout: 0 - # -- CA that signed the OIDC provider cert. Passed through as a Helm multi-line string. - certificate_authority: "" - - # -- URI for Redis instance used for OIDC token storage/retrieval. This may also be specified per-chain, example: tcp://redis:6379/ - redis_server_uri: "" - - oidc: - # -- OpenID Connect hostname. Assumption of Keycloak based on URL construction - host: login.uds.dev - # -- Realm for OpenID Connect - realm: doug - - # -- JWKS, a default jwks_uri is computed if not specified. Must be formatted as an escaped JSON string. - jwks: "" - - # -- Request interval to check whether new JWKs are available. - periodic_fetch_interval_sec: 60 - - # -- If set to true, the verification of the destination certificate will be skipped when making a request to the JWKs URI and the token endpoint. This option is useful when you want to use a self-signed certificate for testing purposes, but basically should not be set to true in any other cases. - skip_verify_peer_cert: false - -# -- Individual chains. Must have a `name` value and a `callback_uri`, full example of all fields provided below. -# NOTE: if using "match" can only specify `prefix` OR `equality`, not both -chains: - # Default Filter to prevent errors on launch - local: - match: - header: ":local" - prefix: "localhost" - client_id: local_id - client_secret: local_secret - callback_uri: https://localhost/login - logout_path: "/local" - # example_chain: - # match: - # header: ":authority" - # prefix: "localhost" - # equality: "localhost.localdomain" - # client_id: my_uds_app - # client_secret: secret_value - # callback_uri: https://myapp.uds.dev/login - # cookie_name_prefix: differentThanFull # Override the cookie name prefix in case you need it to be something else (ex. two apps share the same cookie) - # logout: - # path: "/logout" - # absolute_session_timeout: timeout_value - # idle_session_timeout: timeout_value - # jwks_uri: https://myapp.uds.dev/jwks # Override if this client is on a different realm - # oidc: - # host: local_oidc_host - # realm: local_oidc_realm - # periodic_fetch_interval_sec: 60 - # scopes: - # - additionalScope1 - # - additionalScope2 - nameOverride: "authservice" podAnnotations: {} @@ -113,21 +28,3 @@ nodeSelector: {} tolerations: [] affinity: {} - -# -- Log level for the deployment -config: - logLevel: trace - -# -- Label to determine what workloads (pods/deployments) should be protected by authservice. -selector: - key: protect - value: keycloak - -# -- Values to bypass OIDC chains in favor or using istio authorizationpolicies.security.istio.io -# and requestauthentications.security.istio.io for certain endpoints. -trigger_rules: [] -# - excluded_paths: -# - exact: /api/healthcheck -# included_paths: -# - prefix: / -# See reference: https://github.com/istio-ecosystem/authservice/blob/master/docs/README.md diff --git a/src/authservice/common/zarf.yaml b/src/authservice/common/zarf.yaml index 6e728fe16..aa7cefa85 100644 --- a/src/authservice/common/zarf.yaml +++ b/src/authservice/common/zarf.yaml @@ -10,7 +10,7 @@ components: charts: - name: authservice localPath: ../chart - version: 0.5.3 + version: 1.0.1 namespace: authservice actions: onDeploy: diff --git a/src/authservice/values/registry1-values.yaml b/src/authservice/values/registry1-values.yaml index 97fb34ca0..ba6e8f324 100644 --- a/src/authservice/values/registry1-values.yaml +++ b/src/authservice/values/registry1-values.yaml @@ -1,3 +1,3 @@ image: repository: registry1.dso.mil/ironbank/istio-ecosystem/authservice - tag: "0.5.3" + tag: "1.0.1-ubi9" diff --git a/src/authservice/values/upstream-values.yaml b/src/authservice/values/upstream-values.yaml index 1c01b5b26..f4167f3c7 100644 --- a/src/authservice/values/upstream-values.yaml +++ b/src/authservice/values/upstream-values.yaml @@ -1,3 +1,3 @@ image: repository: ghcr.io/istio-ecosystem/authservice/authservice - tag: "0.5.3" + tag: "1.0.1" diff --git a/src/authservice/zarf.yaml b/src/authservice/zarf.yaml index c87b7e7c6..64e6b6f62 100644 --- a/src/authservice/zarf.yaml +++ b/src/authservice/zarf.yaml @@ -16,7 +16,7 @@ components: valuesFiles: - values/upstream-values.yaml images: - - ghcr.io/istio-ecosystem/authservice/authservice:0.5.3 + - ghcr.io/istio-ecosystem/authservice/authservice:1.0.1 - name: authservice required: true @@ -29,4 +29,4 @@ components: valuesFiles: - values/registry1-values.yaml images: - - registry1.dso.mil/ironbank/istio-ecosystem/authservice:0.5.3 + - registry1.dso.mil/ironbank/istio-ecosystem/authservice:1.0.1-ubi9 diff --git a/src/istio/values/values.yaml b/src/istio/values/values.yaml index c7b28d2f4..4b5412489 100644 --- a/src/istio/values/values.yaml +++ b/src/istio/values/values.yaml @@ -6,3 +6,12 @@ meshConfig: holdApplicationUntilProxyStarts: true gatewayTopology: forwardClientCertDetails: SANITIZE + extensionProviders: + - name: "authservice" + envoyExtAuthzGrpc: + service: "authservice.authservice.svc.cluster.local" + port: "10003" + +pilot: + env: + PILOT_JWT_ENABLE_REMOTE_JWKS: hybrid diff --git a/src/metrics-server/chart/templates/service-monitor.yaml b/src/metrics-server/chart/templates/service-monitor.yaml index d7c603693..390875164 100644 --- a/src/metrics-server/chart/templates/service-monitor.yaml +++ b/src/metrics-server/chart/templates/service-monitor.yaml @@ -3,7 +3,7 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: - annotation: + annotations: uds/skip-sm-mutate: "true" name: metrics-server-metrics namespace: metrics-server diff --git a/src/pepr/config.ts b/src/pepr/config.ts index 4946ae793..485d2f80f 100644 --- a/src/pepr/config.ts +++ b/src/pepr/config.ts @@ -1,15 +1,21 @@ import { Component, setupLogger } from "./logger"; let domain = process.env.UDS_DOMAIN; +let caCert = process.env.UDS_CA_CERT; // We need to handle `npx pepr <>` commands that will not template the env vars if (!domain || domain === "###ZARF_VAR_DOMAIN###") { domain = "uds.dev"; } +if (!caCert || caCert === "###ZARF_VAR_CA_CERT###") { + caCert = ""; +} export const UDSConfig = { // Ignore the UDS_DOMAIN if not deployed by Zarf domain, + // Base64 Encoded Trusted CA cert for Istio certificates (i.e. for `sso.domain`) + caCert, // Track if we are running a single test mode isSingleTest: process.env.UDS_SINGLE_TEST === "true", // Allow UDS policy exemptions to be used in any namespace diff --git a/src/pepr/logger.ts b/src/pepr/logger.ts index c300f3e90..8f505faed 100644 --- a/src/pepr/logger.ts +++ b/src/pepr/logger.ts @@ -1,11 +1,13 @@ import { Log } from "pepr"; export enum Component { + STARTUP = "startup", CONFIG = "config", ISTIO = "istio", OPERATOR_EXEMPTIONS = "operator.exemptions", OPERATOR_ISTIO = "operator.istio", OPERATOR_KEYCLOAK = "operator.keycloak", + OPERATOR_AUTHSERVICE = "operator.authservice", OPERATOR_MONITORING = "operator.monitoring", OPERATOR_NETWORK = "operator.network", OPERATOR_GENERATORS = "operator.generators", diff --git a/src/pepr/operator/controllers/istio/virtual-service.ts b/src/pepr/operator/controllers/istio/virtual-service.ts index 591fa691f..3fa892b39 100644 --- a/src/pepr/operator/controllers/istio/virtual-service.ts +++ b/src/pepr/operator/controllers/istio/virtual-service.ts @@ -1,6 +1,6 @@ -import { UDSConfig } from "../../../config"; import { V1OwnerReference } from "@kubernetes/client-node"; -import { Expose, Gateway, IstioVirtualService, IstioHTTP, IstioHTTPRoute } from "../../crd"; +import { UDSConfig } from "../../../config"; +import { Expose, Gateway, IstioHTTP, IstioHTTPRoute, IstioVirtualService } from "../../crd"; import { sanitizeResourceName } from "../utils"; /** diff --git a/src/pepr/operator/controllers/keycloak/authservice/authorization-policy.ts b/src/pepr/operator/controllers/keycloak/authservice/authorization-policy.ts new file mode 100644 index 000000000..0966495c8 --- /dev/null +++ b/src/pepr/operator/controllers/keycloak/authservice/authorization-policy.ts @@ -0,0 +1,172 @@ +import { K8s } from "pepr"; +import { UDSConfig } from "../../../../config"; +import { + IstioAction, + IstioAuthorizationPolicy, + IstioRequestAuthentication, + UDSPackage, +} from "../../../crd"; +import { getOwnerRef } from "../../utils"; +import { log } from "./authservice"; +import { Action as AuthServiceAction, AuthServiceEvent } from "./types"; + +const operationMap: { + [AuthServiceAction.Add]: "Apply"; + [AuthServiceAction.Remove]: "Delete"; +} = { + [AuthServiceAction.Add]: "Apply", + [AuthServiceAction.Remove]: "Delete", +}; + +function authserviceAuthorizationPolicy( + labelSelector: { [key: string]: string }, + name: string, + namespace: string, +): IstioAuthorizationPolicy { + return { + kind: "AuthorizationPolicy", + metadata: { + name: `${name}-authservice`, + namespace, + }, + spec: { + action: IstioAction.Custom, + provider: { + name: "authservice", + }, + rules: [ + { + when: [ + { + key: "request.headers[authorization]", + notValues: ["*"], + }, + ], + }, + ], + selector: { + matchLabels: labelSelector, + }, + }, + }; +} + +function jwtAuthZAuthorizationPolicy( + labelSelector: { [key: string]: string }, + name: string, + namespace: string, +): IstioAuthorizationPolicy { + return { + kind: "AuthorizationPolicy", + metadata: { + name: `${name}-jwt-authz`, + namespace, + }, + spec: { + selector: { + matchLabels: labelSelector, + }, + rules: [ + { + from: [ + { + source: { + requestPrincipals: [`https://sso.${UDSConfig.domain}/realms/uds/*`], + }, + }, + ], + }, + ], + }, + }; +} + +function authNRequestAuthentication( + labelSelector: { [key: string]: string }, + name: string, + namespace: string, +): IstioRequestAuthentication { + return { + kind: "RequestAuthentication", + metadata: { + name: `${name}-jwt-authn`, + namespace, + }, + spec: { + jwtRules: [ + { + audiences: [name], + forwardOriginalToken: true, + issuer: `https://sso.${UDSConfig.domain}/realms/uds`, + jwksUri: `http://keycloak-http.keycloak.svc.cluster.local:8080/realms/uds/protocol/openid-connect/certs`, + }, + ], + selector: { + matchLabels: labelSelector, + }, + }, + }; +} + +async function updatePolicy( + event: AuthServiceEvent, + labelSelector: { [key: string]: string }, + pkg: UDSPackage, +) { + // type safe map event to operation (either Apply or Delete) + const operation = operationMap[event.action]; + const namespace = pkg.metadata!.namespace!; + const generation = (pkg.metadata?.generation ?? 0).toString(); + const ownerReferences = getOwnerRef(pkg); + + const updateMetadata = (resource: IstioAuthorizationPolicy | IstioRequestAuthentication) => { + resource!.metadata!.ownerReferences = ownerReferences; + resource!.metadata!.labels = { + "uds/package": pkg.metadata!.name!, + "uds/generation": generation, + }; + return resource; + }; + + try { + await K8s(IstioAuthorizationPolicy)[operation]( + updateMetadata(authserviceAuthorizationPolicy(labelSelector, event.name, namespace)), + ); + await K8s(IstioRequestAuthentication)[operation]( + updateMetadata(authNRequestAuthentication(labelSelector, event.name, namespace)), + ); + await K8s(IstioAuthorizationPolicy)[operation]( + updateMetadata(jwtAuthZAuthorizationPolicy(labelSelector, event.name, namespace)), + ); + } catch (e) { + const msg = `Failed to update auth policy for ${event.name} in ${namespace}: ${e}`; + log.error(e, msg); + throw new Error(msg, { + cause: e, + }); + } + + try { + await purgeOrphanPolicies(generation, namespace, pkg.metadata!.name!); + } catch (e) { + log.error(e, `Failed to purge orphan auth policies ${event.name} in ${namespace}: ${e}`); + } +} + +async function purgeOrphanPolicies(generation: string, namespace: string, pkgName: string) { + for (const kind of [IstioAuthorizationPolicy, IstioRequestAuthentication]) { + const resources = await K8s(kind) + .InNamespace(namespace) + .WithLabel("uds/package", pkgName) + .Get(); + + for (const resource of resources.items) { + if (resource.metadata?.labels?.["uds/generation"] !== generation) { + log.debug(resource, `Deleting orphaned ${resource.kind!} ${resource.metadata!.name}`); + await K8s(kind).Delete(resource); + } + } + } +} + +export { updatePolicy }; diff --git a/src/pepr/operator/controllers/keycloak/authservice/authservice.spec.ts b/src/pepr/operator/controllers/keycloak/authservice/authservice.spec.ts new file mode 100644 index 000000000..770174196 --- /dev/null +++ b/src/pepr/operator/controllers/keycloak/authservice/authservice.spec.ts @@ -0,0 +1,124 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals"; +import { UDSPackage } from "../../../crd"; +import { Client } from "../types"; +import { updatePolicy } from "./authorization-policy"; +import { buildChain, buildConfig } from "./authservice"; +import * as mockConfig from "./mock-authservice-config.json"; +import { Action, AuthServiceEvent, AuthserviceConfig } from "./types"; + +describe("authservice", () => { + let mockClient: Client; + + beforeEach(() => { + jest.clearAllMocks(); + + mockClient = { + clientId: "test-client", + redirectUris: ["https://foo.uds.dev/login"], + secret: "test-secret", + alwaysDisplayInConsole: false, + attributes: {}, + authenticationFlowBindingOverrides: {}, + bearerOnly: false, + clientAuthenticatorType: "client-secret", + consentRequired: false, + defaultClientScopes: [], + defaultRoles: [], + directAccessGrantsEnabled: false, + enabled: true, + frontchannelLogout: false, + fullScopeAllowed: false, + implicitFlowEnabled: false, + nodeReRegistrationTimeout: 0, + notBefore: 0, + optionalClientScopes: [], + protocol: "openid-connect", + publicClient: false, + serviceAccountsEnabled: false, + standardFlowEnabled: false, + surrogateAuthRequired: false, + webOrigins: [], + }; + }); + + test("should test authservice chain build", async () => { + const chain = buildChain({ + client: mockClient, + name: "sso-client-test", + action: Action.Add, + } as AuthServiceEvent); + expect(chain.name).toEqual("sso-client-test"); + expect(chain.match.prefix).toEqual("foo.uds.dev"); + expect(chain.filters.length).toEqual(1); + + expect(chain.filters[0].oidc_override.authorization_uri).toEqual( + "https://sso.uds.dev/realms/uds/protocol/openid-connect/auth", + ); + + expect(chain.filters[0].oidc_override.client_id).toEqual(mockClient.clientId); + + expect(chain.filters[0].oidc_override.client_secret).toEqual(mockClient.secret); + + expect(chain.filters[0].oidc_override.callback_uri).toEqual(mockClient.redirectUris[0]); + }); + + test("should test authservice chain removal", async () => { + const config = buildConfig(mockConfig as AuthserviceConfig, { + client: mockClient, + name: "local", + action: Action.Remove, + }); + + expect(config.chains.length).toEqual(0); + expect(config.listen_address).toEqual("0.0.0.0"); + }); + + test("should test authservice chain addition", async () => { + let config = buildConfig(mockConfig as AuthserviceConfig, { + client: mockClient, + name: "local", + action: Action.Remove, + }); + + config = buildConfig(config, { client: mockClient, name: "sso-client-a", action: Action.Add }); + config = buildConfig(config, { client: mockClient, name: "sso-client-b", action: Action.Add }); + + expect(config.chains.length).toEqual(2); + }); + + test("should test chain removal by name", async () => { + let config = buildConfig(mockConfig as AuthserviceConfig, { + client: mockClient, + name: "nothere", + action: Action.Remove, + }); + expect(config.chains.length).toEqual(1); + + config = buildConfig(mockConfig as AuthserviceConfig, { + client: mockClient, + name: "local", + action: Action.Remove, + }); + expect(config.chains.length).toEqual(0); + }); + + test("should build an authorization policy", async () => { + const labelSelector = { foo: "bar" }; + const pkg: UDSPackage = { + kind: "Package", + apiVersion: "uds.dev/v1alpha1", + metadata: { + name: "test", + namespace: "default", + generation: 1, + uid: "f50120aa-2713-4502-9496-566b102b1174", + }, + }; + try { + await updatePolicy({ name: "auth-test", action: Action.Add }, labelSelector, pkg); + await updatePolicy({ name: "auth-test", action: Action.Remove }, labelSelector, pkg); + } catch (e) { + expect(e).toBeUndefined(); + } + }); +}); diff --git a/src/pepr/operator/controllers/keycloak/authservice/authservice.ts b/src/pepr/operator/controllers/keycloak/authservice/authservice.ts new file mode 100644 index 000000000..675e931fa --- /dev/null +++ b/src/pepr/operator/controllers/keycloak/authservice/authservice.ts @@ -0,0 +1,121 @@ +import { R } from "pepr"; +import { UDSConfig } from "../../../../config"; +import { Component, setupLogger } from "../../../../logger"; +import { UDSPackage } from "../../../crd"; +import { Client } from "../types"; +import { updatePolicy } from "./authorization-policy"; +import { getAuthserviceConfig, operatorConfig, updateAuthServiceSecret } from "./config"; +import { Action, AuthServiceEvent, AuthserviceConfig, Chain } from "./types"; + +export const log = setupLogger(Component.OPERATOR_AUTHSERVICE); + +export async function authservice(pkg: UDSPackage, clients: Map) { + // Get the list of clients from the package + const authServiceClients = R.filter( + sso => R.isNotNil(sso.enableAuthserviceSelector), + pkg.spec?.sso || [], + ); + + for (const sso of authServiceClients) { + const client = clients.get(sso.clientId); + if (!client) { + throw new Error(`Failed to get client ${sso.clientId}`); + } + + await reconcileAuthservice( + { name: sso.clientId, action: Action.Add, client }, + sso.enableAuthserviceSelector!, + pkg, + ); + } + + const authserviceClients = authServiceClients.map(client => client.clientId); + + await purgeAuthserviceClients(pkg, authserviceClients); + + return authserviceClients; +} + +export async function purgeAuthserviceClients( + pkg: UDSPackage, + newAuthserviceClients: string[] = [], +) { + // compute set difference of pkg.status.authserviceClients and authserviceClients using Ramda + R.difference(pkg.status?.authserviceClients || [], newAuthserviceClients).forEach( + async clientId => { + log.info(`Removing stale authservice chain for client ${clientId}`); + await reconcileAuthservice({ name: clientId, action: Action.Remove }, {}, pkg); + }, + ); +} + +export async function reconcileAuthservice( + event: AuthServiceEvent, + labelSelector: { [key: string]: string }, + pkg: UDSPackage, +) { + await updateConfig(event); + await updatePolicy(event, labelSelector, pkg); +} + +// write authservice config to secret +export async function updateConfig(event: AuthServiceEvent) { + // parse existing authservice config + let config = await getAuthserviceConfig(); + + // update config based on event + config = buildConfig(config, event); + + // update the authservice secret + await updateAuthServiceSecret(config); +} + +export function buildConfig(config: AuthserviceConfig, event: AuthServiceEvent) { + let chains: Chain[]; + + if (event.action == Action.Add) { + // add the new chain to the existing authservice config + chains = config.chains.filter(chain => chain.name !== event.name); + chains = chains.concat(buildChain(event)); + } else if (event.action == Action.Remove) { + // search in the existing chains for the chain to remove by name + chains = config.chains.filter(chain => chain.name !== event.name); + } else { + throw new Error(`Unhandled Action: ${event.action satisfies never}`); + } + + // add the new chains to the existing authservice config + return { ...config, chains } as AuthserviceConfig; +} + +export function buildChain(update: AuthServiceEvent) { + // TODO: get this from the package + // parse the hostname from the first client redirect uri + const hostname = new URL(update.client!.redirectUris[0]).hostname; + + const chain: Chain = { + name: update.name, + match: { + header: ":authority", + prefix: hostname, + }, + filters: [ + { + oidc_override: { + authorization_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/auth`, + token_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/token`, + callback_uri: update.client!.redirectUris[0], + client_id: update.client!.clientId, + client_secret: update.client!.secret, + scopes: [], + logout: { + path: "/local", + redirect_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/token/logout`, + }, + cookie_name_prefix: update.client!.clientId, + }, + }, + ], + }; + return chain; +} diff --git a/src/pepr/operator/controllers/keycloak/authservice/config.ts b/src/pepr/operator/controllers/keycloak/authservice/config.ts new file mode 100644 index 000000000..eace37048 --- /dev/null +++ b/src/pepr/operator/controllers/keycloak/authservice/config.ts @@ -0,0 +1,145 @@ +import { createHash } from "crypto"; + +import { K8s, kind } from "pepr"; +import { UDSConfig } from "../../../../config"; +import { Client } from "../types"; +import { buildChain, log } from "./authservice"; +import { Action, AuthserviceConfig } from "./types"; + +export const operatorConfig = { + namespace: "authservice", + secretName: "authservice-uds", + baseDomain: `https://sso.${UDSConfig.domain}`, + realm: "uds", +}; + +export async function setupAuthserviceSecret() { + if (process.env.PEPR_WATCH_MODE === "true" || process.env.PEPR_MODE === "dev") { + log.info("One-time authservice secret initialization"); + // create namespace if it doesn't exist + await K8s(kind.Namespace).Apply({ + metadata: { + name: operatorConfig.namespace, + }, + }); + + // create secret if it doesn't exist + try { + const secret = await K8s(kind.Secret) + .InNamespace(operatorConfig.namespace) + .Get(operatorConfig.secretName); + log.info(`Authservice Secret exists, skipping creation - ${secret.metadata?.name}`); + } catch (e) { + log.info("Secret does not exist, creating authservice secret"); + try { + await updateAuthServiceSecret(buildInitialSecret(), false); + } catch (err) { + log.error(err, "Failed to create UDS managed authservice secret."); + throw new Error("Failed to create UDS managed authservice secret.", { cause: err }); + } + } + } +} + +// this initial secret is only a placeholder until the first chain is created +function buildInitialSecret(): AuthserviceConfig { + return { + allow_unmatched_requests: false, + listen_address: "0.0.0.0", + listen_port: "10003", + log_level: "info", + default_oidc_config: { + skip_verify_peer_cert: false, + authorization_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/auth`, + token_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/token`, + jwks_fetcher: { + jwks_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/certs`, + periodic_fetch_interval_sec: 60, + }, + client_id: "global_id", + client_secret: "global_secret", + id_token: { + preamble: "Bearer", + header: "Authorization", + }, + trusted_certificate_authority: `${atob(UDSConfig.caCert)}`, + logout: { + path: "/globallogout", + redirect_uri: `https://sso.${UDSConfig.domain}/realms/${operatorConfig.realm}/protocol/openid-connect/token/logout`, + }, + absolute_session_timeout: "0", + idle_session_timeout: "0", + scopes: [], + }, + threads: 8, + chains: [ + buildChain({ + name: "placeholder", + action: Action.Add, + client: { + clientId: "placeholder", + secret: "placeholder", + redirectUris: ["https://localhost/login"], + } as Client, + }), + ], + }; +} + +export async function getAuthserviceConfig() { + const authSvcSecret = await K8s(kind.Secret) + .InNamespace(operatorConfig.namespace) + .Get(operatorConfig.secretName); + return JSON.parse(atob(authSvcSecret!.data!["config.json"])) as AuthserviceConfig; +} + +export async function updateAuthServiceSecret( + authserviceConfig: AuthserviceConfig, + checksum = true, +) { + const config = btoa(JSON.stringify(authserviceConfig)); + const configHash = createHash("sha256").update(config).digest("hex"); + + try { + // write the authservice config to the secret + await K8s(kind.Secret).Apply( + { + metadata: { + namespace: operatorConfig.namespace, + name: operatorConfig.secretName, + }, + data: { + "config.json": config, + }, + }, + { force: true }, + ); + } catch (e) { + log.error(e, `Failed to write authservice secret`); + throw new Error("Failed to write authservice secret", { cause: e }); + } + + log.info("Updated authservice secret successfully"); + + if (checksum) { + log.info("Adding checksum to deployment authservice secret successfully"); + await checksumDeployment(configHash); + } +} + +async function checksumDeployment(checksum: string) { + try { + await K8s(kind.Deployment, { name: "authservice", namespace: operatorConfig.namespace }).Patch([ + { + op: "add", + path: "/spec/template/metadata/annotations/pepr.dev~1checksum", + value: checksum, + }, + ]); + + log.info(`Successfully applied the checksum to authservice`); + } catch (e) { + log.error(`Failed to apply the checksum to authservice: ${e.data?.message}`); + throw new Error("Failed to apply the checksum to authservice", { cause: e }); + } +} diff --git a/src/pepr/operator/controllers/keycloak/authservice/mock-authservice-config.json b/src/pepr/operator/controllers/keycloak/authservice/mock-authservice-config.json new file mode 100644 index 000000000..56cd74938 --- /dev/null +++ b/src/pepr/operator/controllers/keycloak/authservice/mock-authservice-config.json @@ -0,0 +1,60 @@ +{ + "allow_unmatched_requests": false, + "listen_address": "0.0.0.0", + "listen_port": "10003", + "log_level": "trace", + "default_oidc_config": { + "skip_verify_peer_cert": false, + "authorization_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/auth", + "token_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/token", + "jwks_fetcher": { + "jwks_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/certs", + "periodic_fetch_interval_sec": 60, + "skip_verify_peer_cert": false + }, + "client_id": "global_id", + "client_secret": "global_secret", + "id_token": { + "preamble": "Bearer", + "header": "Authorization" + }, + "access_token": { + "header": "JWT" + }, + "trusted_certificate_authority": "", + "logout": { + "path": "/globallogout", + "redirect_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/token/logout" + }, + "absolute_session_timeout": "0", + "idle_session_timeout": "0", + "scopes": [] + }, + "threads": 8, + "chains": [ + { + "name": "local", + "match": { + "header": ":local", + "prefix": "localhost" + }, + "filters": [ + { + "oidc_override": { + "authorization_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/auth", + "token_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/token", + "callback_uri": "https://localhost/login", + "client_id": "local_id", + "client_secret": "local_secret", + "cookie_name_prefix": "local", + "logout": { + "path": "/local", + "redirect_uri": "https://sso.uds.dev/realms/uds/protocol/openid-connect/token/logout" + }, + "scopes": [] + } + } + ] + } + ] +} diff --git a/src/pepr/operator/controllers/keycloak/authservice/types.ts b/src/pepr/operator/controllers/keycloak/authservice/types.ts new file mode 100644 index 000000000..9c20fdc50 --- /dev/null +++ b/src/pepr/operator/controllers/keycloak/authservice/types.ts @@ -0,0 +1,71 @@ +import { Client } from "../types"; + +export enum Action { + Add = "Add", + Remove = "Remove", +} + +export interface AuthServiceEvent { + name: string; + action: Action; + client?: Client; +} + +export interface AuthserviceConfig { + allow_unmatched_requests: boolean; + listen_address: string; + listen_port: string; + log_level: string; + default_oidc_config: OIDCConfig; + threads: number; + chains: Chain[]; +} + +interface OIDCConfig { + skip_verify_peer_cert?: boolean; + authorization_uri: string; + callback_uri?: string; + cookie_name_prefix?: string; + token_uri: string; + jwks_fetcher?: JWKSFetcher; + client_id: string; + client_secret: string; + id_token?: Token; + access_token?: Token; + trusted_certificate_authority?: string; + logout: Logout; + absolute_session_timeout?: string; + idle_session_timeout?: string; + scopes: string[]; +} + +interface JWKSFetcher { + jwks_uri: string; + periodic_fetch_interval_sec: number; + skip_verify_peer_cert?: boolean; +} + +interface Token { + preamble?: string; + header: string; +} + +interface Logout { + path: string; + redirect_uri: string; +} + +export interface Chain { + name: string; + match: Match; + filters: Filter[]; +} + +interface Match { + header: string; + prefix: string; +} + +interface Filter { + oidc_override: OIDCConfig; +} diff --git a/src/pepr/operator/controllers/keycloak/client-sync.ts b/src/pepr/operator/controllers/keycloak/client-sync.ts index bc2d2bd66..da2042370 100644 --- a/src/pepr/operator/controllers/keycloak/client-sync.ts +++ b/src/pepr/operator/controllers/keycloak/client-sync.ts @@ -46,17 +46,16 @@ const log = setupLogger(Component.OPERATOR_KEYCLOAK); export async function keycloak(pkg: UDSPackage) { // Get the list of clients from the package const clientReqs = pkg.spec?.sso || []; - const refs: string[] = []; + const clients: Map = new Map(); - // Pull the isAuthSvcClient prop as it's not part of the KC client spec for (const clientReq of clientReqs) { - const ref = await syncClient(clientReq, pkg); - refs.push(ref); + const client = await syncClient(clientReq, pkg); + clients.set(client.clientId, client); } - await purgeSSOClients(pkg, refs); + await purgeSSOClients(pkg, [...clients.keys()]); - return refs; + return clients; } /** @@ -65,24 +64,25 @@ export async function keycloak(pkg: UDSPackage) { * @param pkg the package to process * @param refs the list of client refs to keep */ -export async function purgeSSOClients(pkg: UDSPackage, refs: string[] = []) { +export async function purgeSSOClients(pkg: UDSPackage, newClients: string[] = []) { // Check for any clients that are no longer in the package and remove them const currentClients = pkg.status?.ssoClients || []; - const toRemove = currentClients.filter(client => !refs.includes(client)); + const toRemove = currentClients.filter(client => !newClients.includes(client)); for (const ref of toRemove) { - const token = Store.getItem(ref); - const clientId = ref.replace("sso-client-", ""); + const storeKey = `sso-client-${ref}`; + const token = Store.getItem(storeKey); if (token) { - Store.removeItem(ref); - await apiCall({ clientId }, "DELETE", token); + await apiCall({ clientId: ref }, "DELETE", token); + Store.removeItem(storeKey); } else { - log.warn(pkg.metadata, `Failed to remove client ${clientId}, token not found`); + log.warn(pkg.metadata, `Failed to remove client ${ref}, token not found`); } } } async function syncClient( - { isAuthSvcClient, secretName, secretTemplate, ...clientReq }: Sso, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { enableAuthserviceSelector, secretName, secretTemplate, ...clientReq }: Sso, pkg: UDSPackage, isRetry = false, ) { @@ -166,11 +166,7 @@ async function syncClient( data: generateSecretData(client, secretTemplate), }); - if (isAuthSvcClient) { - // Do things here - } - - return name; + return client; } /** @@ -217,7 +213,7 @@ async function apiCall(sso: Partial, method = "POST", authToken = "") { } // Remove the body for DELETE requests - if (method === "DELETE") { + if (method === "DELETE" || method === "GET") { delete req.body; } diff --git a/src/pepr/operator/controllers/monitoring/common.ts b/src/pepr/operator/controllers/monitoring/common.ts new file mode 100644 index 000000000..a8afa5d1f --- /dev/null +++ b/src/pepr/operator/controllers/monitoring/common.ts @@ -0,0 +1,12 @@ +import { Monitor } from "../../crd"; +import { sanitizeResourceName } from "../utils"; + +export function generateMonitorName(pkgName: string, monitor: Monitor) { + const { selector, portName, description } = monitor; + + // Ensure the resource name is valid + const nameSuffix = description || `${Object.values(selector)}-${portName}`; + const name = sanitizeResourceName(`${pkgName}-${nameSuffix}`); + + return name; +} diff --git a/src/pepr/operator/controllers/monitoring/pod-monitor.spec.ts b/src/pepr/operator/controllers/monitoring/pod-monitor.spec.ts new file mode 100644 index 000000000..acba54e26 --- /dev/null +++ b/src/pepr/operator/controllers/monitoring/pod-monitor.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "@jest/globals"; +import { Monitor } from "../../crd"; +import { generatePodMonitor } from "./pod-monitor"; + +describe("test generate Pod monitor", () => { + it("should return a valid Pod Monitor object", () => { + const ownerRefs = [ + { + apiVersion: "uds.dev/v1alpha1", + kind: "Package", + name: "test", + uid: "f50120aa-2713-4502-9496-566b102b1174", + }, + ]; + const portName = "http-metrics"; + const metricsPath = "/test"; + const selectorApp = "test"; + const monitor: Monitor = { + portName: portName, + path: metricsPath, + targetPort: 1234, + selector: { + app: selectorApp, + }, + }; + const namespace = "test"; + const pkgName = "test"; + const generation = "1"; + const payload = generatePodMonitor(monitor, namespace, pkgName, generation, ownerRefs); + + expect(payload).toBeDefined(); + expect(payload.metadata?.name).toEqual(`${pkgName}-${selectorApp}-${portName}`); + expect(payload.metadata?.namespace).toEqual(namespace); + expect(payload.spec?.podMetricsEndpoints).toBeDefined(); + if (payload.spec?.podMetricsEndpoints) { + expect(payload.spec.podMetricsEndpoints[0].port).toEqual(portName); + expect(payload.spec.podMetricsEndpoints[0].path).toEqual(metricsPath); + } + expect(payload.spec?.selector.matchLabels).toHaveProperty("app", "test"); + }); +}); diff --git a/src/pepr/operator/controllers/monitoring/pod-monitor.ts b/src/pepr/operator/controllers/monitoring/pod-monitor.ts new file mode 100644 index 000000000..2ac1c2e11 --- /dev/null +++ b/src/pepr/operator/controllers/monitoring/pod-monitor.ts @@ -0,0 +1,103 @@ +import { V1OwnerReference } from "@kubernetes/client-node"; +import { K8s } from "pepr"; +import { Component, setupLogger } from "../../../logger"; +import { Monitor, PrometheusPodMonitor, UDSPackage } from "../../crd"; +import { Kind } from "../../crd/generated/package-v1alpha1"; +import { getOwnerRef } from "../utils"; +import { generateMonitorName } from "./common"; + +// configure subproject logger +const log = setupLogger(Component.OPERATOR_MONITORING); + +/** + * Generate a pod monitor for a pod + * + * @param pkg UDS Package + * @param namespace + */ +export async function podMonitor(pkg: UDSPackage, namespace: string) { + const pkgName = pkg.metadata!.name!; + const generation = (pkg.metadata?.generation ?? 0).toString(); + const ownerRefs = getOwnerRef(pkg); + + log.debug(`Reconciling PodMonitors for ${pkgName}`); + + // Get the list of monitored services + const monitorList = pkg.spec?.monitor ?? []; + + // Create a list of generated PodMonitors + const payloads: PrometheusPodMonitor[] = []; + + try { + for (const monitor of monitorList) { + if (monitor.kind === Kind.PodMonitor) { + const payload = generatePodMonitor(monitor, namespace, pkgName, generation, ownerRefs); + + log.debug(payload, `Applying PodMonitor ${payload.metadata?.name}`); + + // Apply the PodMonitor and force overwrite any existing policy + await K8s(PrometheusPodMonitor).Apply(payload, { force: true }); + + payloads.push(payload); + } + } + + // Get all related PodMonitors in the namespace + const podMonitors = await K8s(PrometheusPodMonitor) + .InNamespace(namespace) + .WithLabel("uds/package", pkgName) + .Get(); + + // Find any orphaned PodMonitors (not matching the current generation) + const orphanedMonitor = podMonitors.items.filter( + m => m.metadata?.labels?.["uds/generation"] !== generation, + ); + + // Delete any orphaned PodMonitors + for (const m of orphanedMonitor) { + log.debug(m, `Deleting orphaned PodMonitor ${m.metadata!.name}`); + await K8s(PrometheusPodMonitor).Delete(m); + } + } catch (err) { + throw new Error(`Failed to process PodMonitors for ${pkgName}, cause: ${JSON.stringify(err)}`); + } + + // Return the list of monitor names + return [...payloads.map(m => m.metadata!.name!)]; +} + +export function generatePodMonitor( + monitor: Monitor, + namespace: string, + pkgName: string, + generation: string, + ownerRefs: V1OwnerReference[], +) { + const { selector, portName } = monitor; + const name = generateMonitorName(pkgName, monitor); + const payload: PrometheusPodMonitor = { + metadata: { + name, + namespace, + labels: { + "uds/package": pkgName, + "uds/generation": generation, + }, + ownerReferences: ownerRefs, + }, + spec: { + podMetricsEndpoints: [ + { + port: portName, + path: monitor.path || "/metrics", + authorization: monitor.authorization, + }, + ], + selector: { + matchLabels: selector, + }, + }, + }; + + return payload; +} diff --git a/src/pepr/operator/controllers/monitoring/service-monitor.spec.ts b/src/pepr/operator/controllers/monitoring/service-monitor.spec.ts index 83d4fa03e..e99900409 100644 --- a/src/pepr/operator/controllers/monitoring/service-monitor.spec.ts +++ b/src/pepr/operator/controllers/monitoring/service-monitor.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@jest/globals"; -import { generateServiceMonitor } from "./service-monitor"; import { Monitor } from "../../crd"; +import { generateServiceMonitor } from "./service-monitor"; describe("test generate service monitor", () => { it("should return a valid Service Monitor object", () => { diff --git a/src/pepr/operator/controllers/monitoring/service-monitor.ts b/src/pepr/operator/controllers/monitoring/service-monitor.ts index be1ddf9ac..641c9e86c 100644 --- a/src/pepr/operator/controllers/monitoring/service-monitor.ts +++ b/src/pepr/operator/controllers/monitoring/service-monitor.ts @@ -2,8 +2,10 @@ import { K8s } from "pepr"; import { V1OwnerReference } from "@kubernetes/client-node"; import { Component, setupLogger } from "../../../logger"; -import { Monitor, Prometheus, UDSPackage } from "../../crd"; -import { getOwnerRef, sanitizeResourceName } from "../utils"; +import { Monitor, PrometheusServiceMonitor, UDSPackage } from "../../crd"; +import { Kind } from "../../crd/generated/package-v1alpha1"; +import { getOwnerRef } from "../utils"; +import { generateMonitorName } from "./common"; // configure subproject logger const log = setupLogger(Component.OPERATOR_MONITORING); @@ -25,35 +27,37 @@ export async function serviceMonitor(pkg: UDSPackage, namespace: string) { const monitorList = pkg.spec?.monitor ?? []; // Create a list of generated ServiceMonitors - const payloads: Prometheus.ServiceMonitor[] = []; + const payloads: PrometheusServiceMonitor[] = []; try { for (const monitor of monitorList) { - const payload = generateServiceMonitor(monitor, namespace, pkgName, generation, ownerRefs); + if (monitor.kind !== Kind.PodMonitor) { + const payload = generateServiceMonitor(monitor, namespace, pkgName, generation, ownerRefs); - log.debug(payload, `Applying ServiceMonitor ${payload.metadata?.name}`); + log.debug(payload, `Applying ServiceMonitor ${payload.metadata?.name}`); - // Apply the ServiceMonitor and force overwrite any existing policy - await K8s(Prometheus.ServiceMonitor).Apply(payload, { force: true }); + // Apply the ServiceMonitor and force overwrite any existing policy + await K8s(PrometheusServiceMonitor).Apply(payload, { force: true }); - payloads.push(payload); + payloads.push(payload); + } } // Get all related ServiceMonitors in the namespace - const serviceMonitors = await K8s(Prometheus.ServiceMonitor) + const serviceMonitors = await K8s(PrometheusServiceMonitor) .InNamespace(namespace) .WithLabel("uds/package", pkgName) .Get(); // Find any orphaned ServiceMonitors (not matching the current generation) - const orphanedSM = serviceMonitors.items.filter( - sm => sm.metadata?.labels?.["uds/generation"] !== generation, + const orphanedMonitor = serviceMonitors.items.filter( + m => m.metadata?.labels?.["uds/generation"] !== generation, ); // Delete any orphaned ServiceMonitors - for (const sm of orphanedSM) { - log.debug(sm, `Deleting orphaned ServiceMonitor ${sm.metadata!.name}`); - await K8s(Prometheus.ServiceMonitor).Delete(sm); + for (const m of orphanedMonitor) { + log.debug(m, `Deleting orphaned ServiceMonitor ${m.metadata!.name}`); + await K8s(PrometheusServiceMonitor).Delete(m); } } catch (err) { throw new Error( @@ -62,17 +66,7 @@ export async function serviceMonitor(pkg: UDSPackage, namespace: string) { } // Return the list of monitor names - return [...payloads.map(sm => sm.metadata!.name!)]; -} - -export function generateSMName(pkgName: string, monitor: Monitor) { - const { selector, portName, description } = monitor; - - // Ensure the resource name is valid - const nameSuffix = description || `${Object.values(selector)}-${portName}`; - const name = sanitizeResourceName(`${pkgName}-${nameSuffix}`); - - return name; + return [...payloads.map(m => m.metadata!.name!)]; } export function generateServiceMonitor( @@ -83,8 +77,8 @@ export function generateServiceMonitor( ownerRefs: V1OwnerReference[], ) { const { selector, portName } = monitor; - const name = generateSMName(pkgName, monitor); - const payload: Prometheus.ServiceMonitor = { + const name = generateMonitorName(pkgName, monitor); + const payload: PrometheusServiceMonitor = { metadata: { name, namespace, @@ -99,6 +93,7 @@ export function generateServiceMonitor( { port: portName, path: monitor.path || "/metrics", + authorization: monitor.authorization, }, ], selector: { diff --git a/src/pepr/operator/controllers/network/policies.ts b/src/pepr/operator/controllers/network/policies.ts index f12c775b6..2f7e13daa 100644 --- a/src/pepr/operator/controllers/network/policies.ts +++ b/src/pepr/operator/controllers/network/policies.ts @@ -67,6 +67,37 @@ export async function networkPolicies(pkg: UDSPackage, namespace: string) { policies.push(generatedPolicy); } + // Add a network policy for each sso block with authservice enabled (if any pkg.spec.sso[*].enableAuthserviceSelector is set) + const ssos = pkg.spec?.sso?.filter(sso => sso.enableAuthserviceSelector); + + for (const sso of ssos || []) { + const policy: Allow = { + direction: Direction.Egress, + selector: sso.enableAuthserviceSelector, + remoteNamespace: "authservice", + remoteSelector: { "app.kubernetes.io/name": "authservice" }, + port: 10003, + description: `${sanitizeResourceName(sso.clientId)} authservice egress`, + }; + + // Generate the workload to keycloak for JWKS endpoint policy + const generatedPolicy = generate(namespace, policy); + policies.push(generatedPolicy); + + const keycloakPolicy: Allow = { + direction: Direction.Egress, + selector: sso.enableAuthserviceSelector, + remoteNamespace: "keycloak", + remoteSelector: { "app.kubernetes.io/name": "keycloak" }, + port: 8080, + description: `${sanitizeResourceName(sso.clientId)} keycloak JWKS egress`, + }; + + // Generate the policy + const keycloakGeneratedPolicy = generate(namespace, keycloakPolicy); + policies.push(keycloakGeneratedPolicy); + } + // Generate NetworkPolicies for any ServiceMonitors that are generated const monitorList = pkg.spec?.monitor ?? []; // Iterate over each ServiceMonitor diff --git a/src/pepr/operator/crd/generated/istio/authorizationpolicy-v1beta1.ts b/src/pepr/operator/crd/generated/istio/authorizationpolicy-v1beta1.ts new file mode 100644 index 000000000..f05d62b50 --- /dev/null +++ b/src/pepr/operator/crd/generated/istio/authorizationpolicy-v1beta1.ts @@ -0,0 +1,227 @@ +// This file is auto-generated by kubernetes-fluent-client, do not edit manually + +import { GenericKind, RegisterKind } from "kubernetes-fluent-client"; + +export class AuthorizationPolicy extends GenericKind { + /** + * Configuration for access control on workloads. See more details at: + * https://istio.io/docs/reference/config/security/authorization-policy.html + */ + spec?: Spec; + status?: { [key: string]: unknown }; +} + +/** + * Configuration for access control on workloads. See more details at: + * https://istio.io/docs/reference/config/security/authorization-policy.html + */ +export interface Spec { + /** + * Optional. + */ + action?: Action; + /** + * Specifies detailed configuration of the CUSTOM action. + */ + provider?: Provider; + /** + * Optional. + */ + rules?: Rule[]; + /** + * Optional. + */ + selector?: Selector; + /** + * Optional. + */ + targetRef?: TargetRef; +} + +/** + * Optional. + */ +export enum Action { + Allow = "ALLOW", + Audit = "AUDIT", + Custom = "CUSTOM", + Deny = "DENY", +} + +/** + * Specifies detailed configuration of the CUSTOM action. + */ +export interface Provider { + /** + * Specifies the name of the extension provider. + */ + name?: string; +} + +export interface Rule { + /** + * Optional. + */ + from?: From[]; + /** + * Optional. + */ + to?: To[]; + /** + * Optional. + */ + when?: When[]; +} + +export interface From { + /** + * Source specifies the source of a request. + */ + source?: Source; +} + +/** + * Source specifies the source of a request. + */ +export interface Source { + /** + * Optional. + */ + ipBlocks?: string[]; + /** + * Optional. + */ + namespaces?: string[]; + /** + * Optional. + */ + notIpBlocks?: string[]; + /** + * Optional. + */ + notNamespaces?: string[]; + /** + * Optional. + */ + notPrincipals?: string[]; + /** + * Optional. + */ + notRemoteIpBlocks?: string[]; + /** + * Optional. + */ + notRequestPrincipals?: string[]; + /** + * Optional. + */ + principals?: string[]; + /** + * Optional. + */ + remoteIpBlocks?: string[]; + /** + * Optional. + */ + requestPrincipals?: string[]; +} + +export interface To { + /** + * Operation specifies the operation of a request. + */ + operation?: Operation; +} + +/** + * Operation specifies the operation of a request. + */ +export interface Operation { + /** + * Optional. + */ + hosts?: string[]; + /** + * Optional. + */ + methods?: string[]; + /** + * Optional. + */ + notHosts?: string[]; + /** + * Optional. + */ + notMethods?: string[]; + /** + * Optional. + */ + notPaths?: string[]; + /** + * Optional. + */ + notPorts?: string[]; + /** + * Optional. + */ + paths?: string[]; + /** + * Optional. + */ + ports?: string[]; +} + +export interface When { + /** + * The name of an Istio attribute. + */ + key: string; + /** + * Optional. + */ + notValues?: string[]; + /** + * Optional. + */ + values?: string[]; +} + +/** + * Optional. + */ +export interface Selector { + /** + * One or more labels that indicate a specific set of pods/VMs on which a policy should be + * applied. + */ + matchLabels?: { [key: string]: string }; +} + +/** + * Optional. + */ +export interface TargetRef { + /** + * group is the group of the target resource. + */ + group?: string; + /** + * kind is kind of the target resource. + */ + kind?: string; + /** + * name is the name of the target resource. + */ + name?: string; + /** + * namespace is the namespace of the referent. + */ + namespace?: string; +} + +RegisterKind(AuthorizationPolicy, { + group: "security.istio.io", + version: "v1beta1", + kind: "AuthorizationPolicy", + plural: "authorizationpolicies", +}); diff --git a/src/pepr/operator/crd/generated/istio/requestauthentication-v1.ts b/src/pepr/operator/crd/generated/istio/requestauthentication-v1.ts new file mode 100644 index 000000000..ecf85a878 --- /dev/null +++ b/src/pepr/operator/crd/generated/istio/requestauthentication-v1.ts @@ -0,0 +1,138 @@ +// This file is auto-generated by kubernetes-fluent-client, do not edit manually + +import { GenericKind, RegisterKind } from "kubernetes-fluent-client"; + +export class RequestAuthentication extends GenericKind { + /** + * Request authentication configuration for workloads. See more details at: + * https://istio.io/docs/reference/config/security/request_authentication.html + */ + spec?: Spec; + status?: { [key: string]: unknown }; +} + +/** + * Request authentication configuration for workloads. See more details at: + * https://istio.io/docs/reference/config/security/request_authentication.html + */ +export interface Spec { + /** + * Define the list of JWTs that can be validated at the selected workloads' proxy. + */ + jwtRules?: JwtRule[]; + /** + * Optional. + */ + selector?: Selector; + /** + * Optional. + */ + targetRef?: TargetRef; +} + +export interface JwtRule { + /** + * The list of JWT [audiences](https://tools.ietf.org/html/rfc7519#section-4.1.3) that are + * allowed to access. + */ + audiences?: string[]; + /** + * If set to true, the original token will be kept for the upstream request. + */ + forwardOriginalToken?: boolean; + /** + * List of header locations from which JWT is expected. + */ + fromHeaders?: FromHeader[]; + /** + * List of query parameters from which JWT is expected. + */ + fromParams?: string[]; + /** + * Identifies the issuer that issued the JWT. + */ + issuer: string; + /** + * JSON Web Key Set of public keys to validate signature of the JWT. + */ + jwks?: string; + /** + * URL of the provider's public key set to validate signature of the JWT. + */ + jwks_uri?: string; + /** + * URL of the provider's public key set to validate signature of the JWT. + */ + jwksUri?: string; + /** + * This field specifies a list of operations to copy the claim to HTTP headers on a + * successfully verified token. + */ + outputClaimToHeaders?: OutputClaimToHeader[]; + /** + * This field specifies the header name to output a successfully verified JWT payload to the + * backend. + */ + outputPayloadToHeader?: string; +} + +export interface FromHeader { + /** + * The HTTP header name. + */ + name: string; + /** + * The prefix that should be stripped before decoding the token. + */ + prefix?: string; +} + +export interface OutputClaimToHeader { + /** + * The name of the claim to be copied from. + */ + claim?: string; + /** + * The name of the header to be created. + */ + header?: string; +} + +/** + * Optional. + */ +export interface Selector { + /** + * One or more labels that indicate a specific set of pods/VMs on which a policy should be + * applied. + */ + matchLabels?: { [key: string]: string }; +} + +/** + * Optional. + */ +export interface TargetRef { + /** + * group is the group of the target resource. + */ + group?: string; + /** + * kind is kind of the target resource. + */ + kind?: string; + /** + * name is the name of the target resource. + */ + name?: string; + /** + * namespace is the namespace of the referent. + */ + namespace?: string; +} + +RegisterKind(RequestAuthentication, { + group: "security.istio.io", + version: "v1", + kind: "RequestAuthentication", +}); diff --git a/src/pepr/operator/crd/generated/package-v1alpha1.ts b/src/pepr/operator/crd/generated/package-v1alpha1.ts index 71f70981f..a96450297 100644 --- a/src/pepr/operator/crd/generated/package-v1alpha1.ts +++ b/src/pepr/operator/crd/generated/package-v1alpha1.ts @@ -9,7 +9,7 @@ export class Package extends GenericKind { export interface Spec { /** - * Create Service Monitor configurations + * Create Service or Pod Monitor configurations */ monitor?: Monitor[]; /** @@ -23,10 +23,19 @@ export interface Spec { } export interface Monitor { + /** + * Authorization settings. + */ + authorization?: Authorization; /** * A description of this monitor entry, this will become part of the ServiceMonitor name */ description?: string; + /** + * The type of monitor to create; PodMonitor or ServiceMonitor. ServiceMonitor is the + * default. + */ + kind?: Kind; /** * HTTP path from which to scrape for metrics, defaults to `/metrics` */ @@ -51,6 +60,51 @@ export interface Monitor { targetPort: number; } +/** + * Authorization settings. + */ +export interface Authorization { + /** + * Selects a key of a Secret in the namespace that contains the credentials for + * authentication. + */ + credentials: Credentials; + /** + * Defines the authentication type. The value is case-insensitive. "Basic" is not a + * supported value. Default: "Bearer" + */ + type?: string; +} + +/** + * Selects a key of a Secret in the namespace that contains the credentials for + * authentication. + */ +export interface Credentials { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. More info: + * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * The type of monitor to create; PodMonitor or ServiceMonitor. ServiceMonitor is the + * default. + */ +export enum Kind { + PodMonitor = "PodMonitor", + ServiceMonitor = "ServiceMonitor", +} + /** * Network configuration for the package */ @@ -474,6 +528,11 @@ export interface Sso { * A description for the client, can be a URL to an image to replace the login logo */ description?: string; + /** + * Labels to match pods to automatically protect with authservice. Leave empty to disable + * authservice protection + */ + enableAuthserviceSelector?: { [key: string]: string }; /** * Whether the SSO client is enabled */ @@ -482,10 +541,6 @@ export interface Sso { * The client sso group type */ groups?: Groups; - /** - * If true, the client will generate a new Auth Service client as well - */ - isAuthSvcClient?: boolean; /** * Specifies display name of the client */ @@ -549,6 +604,7 @@ export enum Protocol { } export interface Status { + authserviceClients?: string[]; endpoints?: string[]; monitors?: string[]; networkPolicyCount?: number; diff --git a/src/pepr/operator/crd/generated/prometheus/podmonitor-v1.ts b/src/pepr/operator/crd/generated/prometheus/podmonitor-v1.ts new file mode 100644 index 000000000..d2e9f3f9a --- /dev/null +++ b/src/pepr/operator/crd/generated/prometheus/podmonitor-v1.ts @@ -0,0 +1,1011 @@ +// This file is auto-generated by kubernetes-fluent-client, do not edit manually + +import { GenericKind, RegisterKind } from "kubernetes-fluent-client"; + +/** + * PodMonitor defines monitoring for a set of pods. + */ +export class PodMonitor extends GenericKind { + /** + * Specification of desired Pod selection for target discovery by Prometheus. + */ + spec?: Spec; +} + +/** + * Specification of desired Pod selection for target discovery by Prometheus. + */ +export interface Spec { + /** + * `attachMetadata` defines additional metadata which is added to the + * discovered targets. + * + * + * It requires Prometheus >= v2.37.0. + */ + attachMetadata?: AttachMetadata; + /** + * When defined, bodySizeLimit specifies a job level limit on the size + * of uncompressed response body that will be accepted by Prometheus. + * + * + * It requires Prometheus >= v2.28.0. + */ + bodySizeLimit?: string; + /** + * The label to use to retrieve the job name from. + * `jobLabel` selects the label from the associated Kubernetes `Pod` + * object which will be used as the `job` label for all metrics. + * + * + * For example if `jobLabel` is set to `foo` and the Kubernetes `Pod` + * object is labeled with `foo: bar`, then Prometheus adds the `job="bar"` + * label to all ingested metrics. + * + * + * If the value of this field is empty, the `job` label of the metrics + * defaults to the namespace and name of the PodMonitor object (e.g. `/`). + */ + jobLabel?: string; + /** + * Per-scrape limit on the number of targets dropped by relabeling + * that will be kept in memory. 0 means no limit. + * + * + * It requires Prometheus >= v2.47.0. + */ + keepDroppedTargets?: number; + /** + * Per-scrape limit on number of labels that will be accepted for a sample. + * + * + * It requires Prometheus >= v2.27.0. + */ + labelLimit?: number; + /** + * Per-scrape limit on length of labels name that will be accepted for a sample. + * + * + * It requires Prometheus >= v2.27.0. + */ + labelNameLengthLimit?: number; + /** + * Per-scrape limit on length of labels value that will be accepted for a sample. + * + * + * It requires Prometheus >= v2.27.0. + */ + labelValueLengthLimit?: number; + /** + * Selector to select which namespaces the Kubernetes `Pods` objects + * are discovered from. + */ + namespaceSelector?: NamespaceSelector; + /** + * List of endpoints part of this PodMonitor. + */ + podMetricsEndpoints?: PodMetricsEndpoint[]; + /** + * `podTargetLabels` defines the labels which are transferred from the + * associated Kubernetes `Pod` object onto the ingested metrics. + */ + podTargetLabels?: string[]; + /** + * `sampleLimit` defines a per-scrape limit on the number of scraped samples + * that will be accepted. + */ + sampleLimit?: number; + /** + * The scrape class to apply. + */ + scrapeClass?: string; + /** + * `scrapeProtocols` defines the protocols to negotiate during a scrape. It tells clients + * the + * protocols supported by Prometheus in order of preference (from most to least + * preferred). + * + * + * If unset, Prometheus uses its default value. + * + * + * It requires Prometheus >= v2.49.0. + */ + scrapeProtocols?: ScrapeProtocol[]; + /** + * Label selector to select the Kubernetes `Pod` objects. + */ + selector: Selector; + /** + * `targetLimit` defines a limit on the number of scraped targets that will + * be accepted. + */ + targetLimit?: number; +} + +/** + * `attachMetadata` defines additional metadata which is added to the + * discovered targets. + * + * + * It requires Prometheus >= v2.37.0. + */ +export interface AttachMetadata { + /** + * When set to true, Prometheus must have the `get` permission on the + * `Nodes` objects. + */ + node?: boolean; +} + +/** + * Selector to select which namespaces the Kubernetes `Pods` objects + * are discovered from. + */ +export interface NamespaceSelector { + /** + * Boolean describing whether all namespaces are selected in contrast to a + * list restricting them. + */ + any?: boolean; + /** + * List of namespace names to select from. + */ + matchNames?: string[]; +} + +/** + * PodMetricsEndpoint defines an endpoint serving Prometheus metrics to be scraped by + * Prometheus. + */ +export interface PodMetricsEndpoint { + /** + * `authorization` configures the Authorization header credentials to use when + * scraping the target. + * + * + * Cannot be set at the same time as `basicAuth`, or `oauth2`. + */ + authorization?: Authorization; + /** + * `basicAuth` configures the Basic Authentication credentials to use when + * scraping the target. + * + * + * Cannot be set at the same time as `authorization`, or `oauth2`. + */ + basicAuth?: BasicAuth; + /** + * `bearerTokenSecret` specifies a key of a Secret containing the bearer + * token for scraping targets. The secret needs to be in the same namespace + * as the PodMonitor object and readable by the Prometheus Operator. + * + * + * Deprecated: use `authorization` instead. + */ + bearerTokenSecret?: BearerTokenSecret; + /** + * `enableHttp2` can be used to disable HTTP2 when scraping the target. + */ + enableHttp2?: boolean; + /** + * When true, the pods which are not running (e.g. either in Failed or + * Succeeded state) are dropped during the target discovery. + * + * + * If unset, the filtering is enabled. + * + * + * More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + */ + filterRunning?: boolean; + /** + * `followRedirects` defines whether the scrape requests should follow HTTP + * 3xx redirects. + */ + followRedirects?: boolean; + /** + * When true, `honorLabels` preserves the metric's labels when they collide + * with the target's labels. + */ + honorLabels?: boolean; + /** + * `honorTimestamps` controls whether Prometheus preserves the timestamps + * when exposed by the target. + */ + honorTimestamps?: boolean; + /** + * Interval at which Prometheus scrapes the metrics from the target. + * + * + * If empty, Prometheus uses the global scrape interval. + */ + interval?: string; + /** + * `metricRelabelings` configures the relabeling rules to apply to the + * samples before ingestion. + */ + metricRelabelings?: MetricRelabeling[]; + /** + * `oauth2` configures the OAuth2 settings to use when scraping the target. + * + * + * It requires Prometheus >= 2.27.0. + * + * + * Cannot be set at the same time as `authorization`, or `basicAuth`. + */ + oauth2?: Oauth2; + /** + * `params` define optional HTTP URL parameters. + */ + params?: { [key: string]: string[] }; + /** + * HTTP path from which to scrape for metrics. + * + * + * If empty, Prometheus uses the default value (e.g. `/metrics`). + */ + path?: string; + /** + * Name of the Pod port which this endpoint refers to. + * + * + * It takes precedence over `targetPort`. + */ + port?: string; + /** + * `proxyURL` configures the HTTP Proxy URL (e.g. + * "http://proxyserver:2195") to go through when scraping the target. + */ + proxyUrl?: string; + /** + * `relabelings` configures the relabeling rules to apply the target's + * metadata labels. + * + * + * The Operator automatically adds relabelings for a few standard Kubernetes fields. + * + * + * The original scrape job's name is available via the `__tmp_prometheus_job_name` label. + * + * + * More info: + * https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + */ + relabelings?: Relabeling[]; + /** + * HTTP scheme to use for scraping. + * + * + * `http` and `https` are the expected values unless you rewrite the + * `__scheme__` label via relabeling. + * + * + * If empty, Prometheus uses the default value `http`. + */ + scheme?: Scheme; + /** + * Timeout after which Prometheus considers the scrape to be failed. + * + * + * If empty, Prometheus uses the global scrape timeout unless it is less + * than the target's scrape interval value in which the latter is used. + */ + scrapeTimeout?: string; + /** + * Name or number of the target port of the `Pod` object behind the Service, the + * port must be specified with container port property. + * + * + * Deprecated: use 'port' instead. + */ + targetPort?: number | string; + /** + * TLS configuration to use when scraping the target. + */ + tlsConfig?: TLSConfig; + /** + * `trackTimestampsStaleness` defines whether Prometheus tracks staleness of + * the metrics that have an explicit timestamp present in scraped data. + * Has no effect if `honorTimestamps` is false. + * + * + * It requires Prometheus >= v2.48.0. + */ + trackTimestampsStaleness?: boolean; +} + +/** + * `authorization` configures the Authorization header credentials to use when + * scraping the target. + * + * + * Cannot be set at the same time as `basicAuth`, or `oauth2`. + */ +export interface Authorization { + /** + * Selects a key of a Secret in the namespace that contains the credentials for + * authentication. + */ + credentials?: Credentials; + /** + * Defines the authentication type. The value is case-insensitive. + * + * + * "Basic" is not a supported value. + * + * + * Default: "Bearer" + */ + type?: string; +} + +/** + * Selects a key of a Secret in the namespace that contains the credentials for + * authentication. + */ +export interface Credentials { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * `basicAuth` configures the Basic Authentication credentials to use when + * scraping the target. + * + * + * Cannot be set at the same time as `authorization`, or `oauth2`. + */ +export interface BasicAuth { + /** + * `password` specifies a key of a Secret containing the password for + * authentication. + */ + password?: Password; + /** + * `username` specifies a key of a Secret containing the username for + * authentication. + */ + username?: Username; +} + +/** + * `password` specifies a key of a Secret containing the password for + * authentication. + */ +export interface Password { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * `username` specifies a key of a Secret containing the username for + * authentication. + */ +export interface Username { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * `bearerTokenSecret` specifies a key of a Secret containing the bearer + * token for scraping targets. The secret needs to be in the same namespace + * as the PodMonitor object and readable by the Prometheus Operator. + * + * + * Deprecated: use `authorization` instead. + */ +export interface BearerTokenSecret { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + * scraped samples and remote write samples. + * + * + * More info: + * https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + */ +export interface MetricRelabeling { + /** + * Action to perform based on the regex matching. + * + * + * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + * `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + * + * + * Default: "Replace" + */ + action?: Action; + /** + * Modulus to take of the hash of the source label values. + * + * + * Only applicable when the action is `HashMod`. + */ + modulus?: number; + /** + * Regular expression against which the extracted value is matched. + */ + regex?: string; + /** + * Replacement value against which a Replace action is performed if the + * regular expression matches. + * + * + * Regex capture groups are available. + */ + replacement?: string; + /** + * Separator is the string between concatenated SourceLabels. + */ + separator?: string; + /** + * The source labels select values from existing labels. Their content is + * concatenated using the configured Separator and matched against the + * configured regular expression. + */ + sourceLabels?: string[]; + /** + * Label to which the resulting string is written in a replacement. + * + * + * It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + * `KeepEqual` and `DropEqual` actions. + * + * + * Regex capture groups are available. + */ + targetLabel?: string; +} + +/** + * Action to perform based on the regex matching. + * + * + * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + * `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + * + * + * Default: "Replace" + */ +export enum Action { + ActionDrop = "Drop", + ActionKeep = "Keep", + ActionLowercase = "Lowercase", + ActionReplace = "Replace", + ActionUppercase = "Uppercase", + Drop = "drop", + DropEqual = "DropEqual", + Dropequal = "dropequal", + HashMod = "HashMod", + Hashmod = "hashmod", + Keep = "keep", + KeepEqual = "KeepEqual", + Keepequal = "keepequal", + LabelDrop = "LabelDrop", + LabelKeep = "LabelKeep", + LabelMap = "LabelMap", + Labeldrop = "labeldrop", + Labelkeep = "labelkeep", + Labelmap = "labelmap", + Lowercase = "lowercase", + Replace = "replace", + Uppercase = "uppercase", +} + +/** + * `oauth2` configures the OAuth2 settings to use when scraping the target. + * + * + * It requires Prometheus >= 2.27.0. + * + * + * Cannot be set at the same time as `authorization`, or `basicAuth`. + */ +export interface Oauth2 { + /** + * `clientId` specifies a key of a Secret or ConfigMap containing the + * OAuth2 client's ID. + */ + clientId: ClientID; + /** + * `clientSecret` specifies a key of a Secret containing the OAuth2 + * client's secret. + */ + clientSecret: ClientSecret; + /** + * `endpointParams` configures the HTTP parameters to append to the token + * URL. + */ + endpointParams?: { [key: string]: string }; + /** + * `scopes` defines the OAuth2 scopes used for the token request. + */ + scopes?: string[]; + /** + * `tokenURL` configures the URL to fetch the token from. + */ + tokenUrl: string; +} + +/** + * `clientId` specifies a key of a Secret or ConfigMap containing the + * OAuth2 client's ID. + */ +export interface ClientID { + /** + * ConfigMap containing data to use for the targets. + */ + configMap?: ClientIDConfigMap; + /** + * Secret containing data to use for the targets. + */ + secret?: ClientIDSecret; +} + +/** + * ConfigMap containing data to use for the targets. + */ +export interface ClientIDConfigMap { + /** + * The key to select. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the ConfigMap or its key must be defined + */ + optional?: boolean; +} + +/** + * Secret containing data to use for the targets. + */ +export interface ClientIDSecret { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * `clientSecret` specifies a key of a Secret containing the OAuth2 + * client's secret. + */ +export interface ClientSecret { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + * scraped samples and remote write samples. + * + * + * More info: + * https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + */ +export interface Relabeling { + /** + * Action to perform based on the regex matching. + * + * + * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + * `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + * + * + * Default: "Replace" + */ + action?: Action; + /** + * Modulus to take of the hash of the source label values. + * + * + * Only applicable when the action is `HashMod`. + */ + modulus?: number; + /** + * Regular expression against which the extracted value is matched. + */ + regex?: string; + /** + * Replacement value against which a Replace action is performed if the + * regular expression matches. + * + * + * Regex capture groups are available. + */ + replacement?: string; + /** + * Separator is the string between concatenated SourceLabels. + */ + separator?: string; + /** + * The source labels select values from existing labels. Their content is + * concatenated using the configured Separator and matched against the + * configured regular expression. + */ + sourceLabels?: string[]; + /** + * Label to which the resulting string is written in a replacement. + * + * + * It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + * `KeepEqual` and `DropEqual` actions. + * + * + * Regex capture groups are available. + */ + targetLabel?: string; +} + +/** + * HTTP scheme to use for scraping. + * + * + * `http` and `https` are the expected values unless you rewrite the + * `__scheme__` label via relabeling. + * + * + * If empty, Prometheus uses the default value `http`. + */ +export enum Scheme { + HTTP = "http", + HTTPS = "https", +} + +/** + * TLS configuration to use when scraping the target. + */ +export interface TLSConfig { + /** + * Certificate authority used when verifying server certificates. + */ + ca?: CA; + /** + * Client certificate to present when doing client-authentication. + */ + cert?: CERT; + /** + * Disable target certificate validation. + */ + insecureSkipVerify?: boolean; + /** + * Secret containing the client key file for the targets. + */ + keySecret?: KeySecret; + /** + * Used to verify the hostname for the targets. + */ + serverName?: string; +} + +/** + * Certificate authority used when verifying server certificates. + */ +export interface CA { + /** + * ConfigMap containing data to use for the targets. + */ + configMap?: CAConfigMap; + /** + * Secret containing data to use for the targets. + */ + secret?: CASecret; +} + +/** + * ConfigMap containing data to use for the targets. + */ +export interface CAConfigMap { + /** + * The key to select. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the ConfigMap or its key must be defined + */ + optional?: boolean; +} + +/** + * Secret containing data to use for the targets. + */ +export interface CASecret { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * Client certificate to present when doing client-authentication. + */ +export interface CERT { + /** + * ConfigMap containing data to use for the targets. + */ + configMap?: CERTConfigMap; + /** + * Secret containing data to use for the targets. + */ + secret?: CERTSecret; +} + +/** + * ConfigMap containing data to use for the targets. + */ +export interface CERTConfigMap { + /** + * The key to select. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the ConfigMap or its key must be defined + */ + optional?: boolean; +} + +/** + * Secret containing data to use for the targets. + */ +export interface CERTSecret { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * Secret containing the client key file for the targets. + */ +export interface KeySecret { + /** + * The key of the secret to select from. Must be a valid secret key. + */ + key: string; + /** + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + */ + name?: string; + /** + * Specify whether the Secret or its key must be defined + */ + optional?: boolean; +} + +/** + * ScrapeProtocol represents a protocol used by Prometheus for scraping metrics. + * Supported values are: + * * `OpenMetricsText0.0.1` + * * `OpenMetricsText1.0.0` + * * `PrometheusProto` + * * `PrometheusText0.0.4` + */ +export enum ScrapeProtocol { + OpenMetricsText001 = "OpenMetricsText0.0.1", + OpenMetricsText100 = "OpenMetricsText1.0.0", + PrometheusProto = "PrometheusProto", + PrometheusText004 = "PrometheusText0.0.4", +} + +/** + * Label selector to select the Kubernetes `Pod` objects. + */ +export interface Selector { + /** + * matchExpressions is a list of label selector requirements. The requirements are ANDed. + */ + matchExpressions?: MatchExpression[]; + /** + * matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + * map is equivalent to an element of matchExpressions, whose key field is "key", the + * operator is "In", and the values array contains only "value". The requirements are ANDed. + */ + matchLabels?: { [key: string]: string }; +} + +/** + * A label selector requirement is a selector that contains values, a key, and an operator + * that + * relates the key and values. + */ +export interface MatchExpression { + /** + * key is the label key that the selector applies to. + */ + key: string; + /** + * operator represents a key's relationship to a set of values. + * Valid operators are In, NotIn, Exists and DoesNotExist. + */ + operator: string; + /** + * values is an array of string values. If the operator is In or NotIn, + * the values array must be non-empty. If the operator is Exists or DoesNotExist, + * the values array must be empty. This array is replaced during a strategic + * merge patch. + */ + values?: string[]; +} + +RegisterKind(PodMonitor, { + group: "monitoring.coreos.com", + version: "v1", + kind: "PodMonitor", + plural: "podmonitors", +}); diff --git a/src/pepr/operator/crd/generated/prometheus/servicemonitor-v1.ts b/src/pepr/operator/crd/generated/prometheus/servicemonitor-v1.ts index 4d776ccd4..17c09c2a4 100644 --- a/src/pepr/operator/crd/generated/prometheus/servicemonitor-v1.ts +++ b/src/pepr/operator/crd/generated/prometheus/servicemonitor-v1.ts @@ -7,121 +7,178 @@ import { GenericKind, RegisterKind } from "kubernetes-fluent-client"; */ export class ServiceMonitor extends GenericKind { /** - * Specification of desired Service selection for target discovery by Prometheus. + * Specification of desired Service selection for target discovery by + * Prometheus. */ spec?: Spec; } /** - * Specification of desired Service selection for target discovery by Prometheus. + * Specification of desired Service selection for target discovery by + * Prometheus. */ export interface Spec { /** - * `attachMetadata` defines additional metadata which is added to the discovered targets. + * `attachMetadata` defines additional metadata which is added to the + * discovered targets. + * + * * It requires Prometheus >= v2.37.0. */ attachMetadata?: AttachMetadata; + /** + * When defined, bodySizeLimit specifies a job level limit on the size + * of uncompressed response body that will be accepted by Prometheus. + * + * + * It requires Prometheus >= v2.28.0. + */ + bodySizeLimit?: string; /** * List of endpoints part of this ServiceMonitor. */ endpoints?: Endpoint[]; /** - * `jobLabel` selects the label from the associated Kubernetes `Service` object which will - * be used as the `job` label for all metrics. - * For example if `jobLabel` is set to `foo` and the Kubernetes `Service` object is labeled - * with `foo: bar`, then Prometheus adds the `job="bar"` label to all ingested metrics. - * If the value of this field is empty or if the label doesn't exist for the given Service, - * the `job` label of the metrics defaults to the name of the associated Kubernetes - * `Service`. + * `jobLabel` selects the label from the associated Kubernetes `Service` + * object which will be used as the `job` label for all metrics. + * + * + * For example if `jobLabel` is set to `foo` and the Kubernetes `Service` + * object is labeled with `foo: bar`, then Prometheus adds the `job="bar"` + * label to all ingested metrics. + * + * + * If the value of this field is empty or if the label doesn't exist for + * the given Service, the `job` label of the metrics defaults to the name + * of the associated Kubernetes `Service`. */ jobLabel?: string; /** - * Per-scrape limit on the number of targets dropped by relabeling that will be kept in - * memory. 0 means no limit. + * Per-scrape limit on the number of targets dropped by relabeling + * that will be kept in memory. 0 means no limit. + * + * * It requires Prometheus >= v2.47.0. */ keepDroppedTargets?: number; /** * Per-scrape limit on number of labels that will be accepted for a sample. + * + * * It requires Prometheus >= v2.27.0. */ labelLimit?: number; /** * Per-scrape limit on length of labels name that will be accepted for a sample. + * + * * It requires Prometheus >= v2.27.0. */ labelNameLengthLimit?: number; /** * Per-scrape limit on length of labels value that will be accepted for a sample. + * + * * It requires Prometheus >= v2.27.0. */ labelValueLengthLimit?: number; /** - * Selector to select which namespaces the Kubernetes `Endpoints` objects are discovered - * from. + * Selector to select which namespaces the Kubernetes `Endpoints` objects + * are discovered from. */ namespaceSelector?: NamespaceSelector; /** - * `podTargetLabels` defines the labels which are transferred from the associated Kubernetes - * `Pod` object onto the ingested metrics. + * `podTargetLabels` defines the labels which are transferred from the + * associated Kubernetes `Pod` object onto the ingested metrics. */ podTargetLabels?: string[]; /** - * `sampleLimit` defines a per-scrape limit on the number of scraped samples that will be - * accepted. + * `sampleLimit` defines a per-scrape limit on the number of scraped samples + * that will be accepted. */ sampleLimit?: number; + /** + * The scrape class to apply. + */ + scrapeClass?: string; + /** + * `scrapeProtocols` defines the protocols to negotiate during a scrape. It tells clients + * the + * protocols supported by Prometheus in order of preference (from most to least + * preferred). + * + * + * If unset, Prometheus uses its default value. + * + * + * It requires Prometheus >= v2.49.0. + */ + scrapeProtocols?: ScrapeProtocol[]; /** * Label selector to select the Kubernetes `Endpoints` objects. */ selector: Selector; /** - * `targetLabels` defines the labels which are transferred from the associated Kubernetes - * `Service` object onto the ingested metrics. + * `targetLabels` defines the labels which are transferred from the + * associated Kubernetes `Service` object onto the ingested metrics. */ targetLabels?: string[]; /** - * `targetLimit` defines a limit on the number of scraped targets that will be accepted. + * `targetLimit` defines a limit on the number of scraped targets that will + * be accepted. */ targetLimit?: number; } /** - * `attachMetadata` defines additional metadata which is added to the discovered targets. + * `attachMetadata` defines additional metadata which is added to the + * discovered targets. + * + * * It requires Prometheus >= v2.37.0. */ export interface AttachMetadata { /** - * When set to true, Prometheus must have the `get` permission on the `Nodes` objects. + * When set to true, Prometheus must have the `get` permission on the + * `Nodes` objects. */ node?: boolean; } /** - * Endpoint defines an endpoint serving Prometheus metrics to be scraped by Prometheus. + * Endpoint defines an endpoint serving Prometheus metrics to be scraped by + * Prometheus. */ export interface Endpoint { /** - * `authorization` configures the Authorization header credentials to use when scraping the - * target. + * `authorization` configures the Authorization header credentials to use when + * scraping the target. + * + * * Cannot be set at the same time as `basicAuth`, or `oauth2`. */ authorization?: Authorization; /** - * `basicAuth` configures the Basic Authentication credentials to use when scraping the - * target. + * `basicAuth` configures the Basic Authentication credentials to use when + * scraping the target. + * + * * Cannot be set at the same time as `authorization`, or `oauth2`. */ basicAuth?: BasicAuth; /** * File to read bearer token for scraping the target. + * + * * Deprecated: use `authorization` instead. */ bearerTokenFile?: string; /** - * `bearerTokenSecret` specifies a key of a Secret containing the bearer token for scraping - * targets. The secret needs to be in the same namespace as the ServiceMonitor object and - * readable by the Prometheus Operator. + * `bearerTokenSecret` specifies a key of a Secret containing the bearer + * token for scraping targets. The secret needs to be in the same namespace + * as the ServiceMonitor object and readable by the Prometheus Operator. + * + * * Deprecated: use `authorization` instead. */ bearerTokenSecret?: BearerTokenSecret; @@ -130,39 +187,50 @@ export interface Endpoint { */ enableHttp2?: boolean; /** - * When true, the pods which are not running (e.g. either in Failed or Succeeded state) are - * dropped during the target discovery. + * When true, the pods which are not running (e.g. either in Failed or + * Succeeded state) are dropped during the target discovery. + * + * * If unset, the filtering is enabled. + * + * * More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase */ filterRunning?: boolean; /** - * `followRedirects` defines whether the scrape requests should follow HTTP 3xx redirects. + * `followRedirects` defines whether the scrape requests should follow HTTP + * 3xx redirects. */ followRedirects?: boolean; /** - * When true, `honorLabels` preserves the metric's labels when they collide with the - * target's labels. + * When true, `honorLabels` preserves the metric's labels when they collide + * with the target's labels. */ honorLabels?: boolean; /** - * `honorTimestamps` controls whether Prometheus preserves the timestamps when exposed by - * the target. + * `honorTimestamps` controls whether Prometheus preserves the timestamps + * when exposed by the target. */ honorTimestamps?: boolean; /** * Interval at which Prometheus scrapes the metrics from the target. + * + * * If empty, Prometheus uses the global scrape interval. */ interval?: string; /** - * `metricRelabelings` configures the relabeling rules to apply to the samples before - * ingestion. + * `metricRelabelings` configures the relabeling rules to apply to the + * samples before ingestion. */ metricRelabelings?: MetricRelabeling[]; /** * `oauth2` configures the OAuth2 settings to use when scraping the target. + * + * * It requires Prometheus >= 2.27.0. + * + * * Cannot be set at the same time as `authorization`, or `basicAuth`. */ oauth2?: Oauth2; @@ -172,44 +240,60 @@ export interface Endpoint { params?: { [key: string]: string[] }; /** * HTTP path from which to scrape for metrics. + * + * * If empty, Prometheus uses the default value (e.g. `/metrics`). */ path?: string; /** * Name of the Service port which this endpoint refers to. + * + * * It takes precedence over `targetPort`. */ port?: string; /** - * `proxyURL` configures the HTTP Proxy URL (e.g. "http://proxyserver:2195") to go through - * when scraping the target. + * `proxyURL` configures the HTTP Proxy URL (e.g. + * "http://proxyserver:2195") to go through when scraping the target. */ proxyUrl?: string; /** - * `relabelings` configures the relabeling rules to apply the target's metadata labels. + * `relabelings` configures the relabeling rules to apply the target's + * metadata labels. + * + * * The Operator automatically adds relabelings for a few standard Kubernetes fields. + * + * * The original scrape job's name is available via the `__tmp_prometheus_job_name` label. + * + * * More info: * https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config */ relabelings?: Relabeling[]; /** * HTTP scheme to use for scraping. - * `http` and `https` are the expected values unless you rewrite the `__scheme__` label via - * relabeling. + * + * + * `http` and `https` are the expected values unless you rewrite the + * `__scheme__` label via relabeling. + * + * * If empty, Prometheus uses the default value `http`. */ scheme?: Scheme; /** * Timeout after which Prometheus considers the scrape to be failed. - * If empty, Prometheus uses the global scrape timeout unless it is less than the target's - * scrape interval value in which the latter is used. + * + * + * If empty, Prometheus uses the global scrape timeout unless it is less + * than the target's scrape interval value in which the latter is used. */ scrapeTimeout?: string; /** - * Name or number of the target port of the `Pod` object behind the Service, the port must - * be specified with container port property. - * Deprecated: use `port` instead. + * Name or number of the target port of the `Pod` object behind the + * Service. The port must be specified with the container's port property. */ targetPort?: number | string; /** @@ -217,17 +301,21 @@ export interface Endpoint { */ tlsConfig?: TLSConfig; /** - * `trackTimestampsStaleness` defines whether Prometheus tracks staleness of the metrics - * that have an explicit timestamp present in scraped data. Has no effect if - * `honorTimestamps` is false. + * `trackTimestampsStaleness` defines whether Prometheus tracks staleness of + * the metrics that have an explicit timestamp present in scraped data. + * Has no effect if `honorTimestamps` is false. + * + * * It requires Prometheus >= v2.48.0. */ trackTimestampsStaleness?: boolean; } /** - * `authorization` configures the Authorization header credentials to use when scraping the - * target. + * `authorization` configures the Authorization header credentials to use when + * scraping the target. + * + * * Cannot be set at the same time as `basicAuth`, or `oauth2`. */ export interface Authorization { @@ -238,7 +326,11 @@ export interface Authorization { credentials?: Credentials; /** * Defines the authentication type. The value is case-insensitive. + * + * * "Basic" is not a supported value. + * + * * Default: "Bearer" */ type?: string; @@ -254,9 +346,14 @@ export interface Credentials { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -266,23 +363,28 @@ export interface Credentials { } /** - * `basicAuth` configures the Basic Authentication credentials to use when scraping the - * target. + * `basicAuth` configures the Basic Authentication credentials to use when + * scraping the target. + * + * * Cannot be set at the same time as `authorization`, or `oauth2`. */ export interface BasicAuth { /** - * `password` specifies a key of a Secret containing the password for authentication. + * `password` specifies a key of a Secret containing the password for + * authentication. */ password?: Password; /** - * `username` specifies a key of a Secret containing the username for authentication. + * `username` specifies a key of a Secret containing the username for + * authentication. */ username?: Username; } /** - * `password` specifies a key of a Secret containing the password for authentication. + * `password` specifies a key of a Secret containing the password for + * authentication. */ export interface Password { /** @@ -290,9 +392,14 @@ export interface Password { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -302,7 +409,8 @@ export interface Password { } /** - * `username` specifies a key of a Secret containing the username for authentication. + * `username` specifies a key of a Secret containing the username for + * authentication. */ export interface Username { /** @@ -310,9 +418,14 @@ export interface Username { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -322,9 +435,11 @@ export interface Username { } /** - * `bearerTokenSecret` specifies a key of a Secret containing the bearer token for scraping - * targets. The secret needs to be in the same namespace as the ServiceMonitor object and - * readable by the Prometheus Operator. + * `bearerTokenSecret` specifies a key of a Secret containing the bearer + * token for scraping targets. The secret needs to be in the same namespace + * as the ServiceMonitor object and readable by the Prometheus Operator. + * + * * Deprecated: use `authorization` instead. */ export interface BearerTokenSecret { @@ -333,9 +448,14 @@ export interface BearerTokenSecret { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -345,21 +465,29 @@ export interface BearerTokenSecret { } /** - * RelabelConfig allows dynamic rewriting of the label set for targets, alerts, scraped - * samples and remote write samples. + * RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + * scraped samples and remote write samples. + * + * * More info: * https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config */ export interface MetricRelabeling { /** * Action to perform based on the regex matching. - * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. `DropEqual` and - * `KeepEqual` actions require Prometheus >= v2.41.0. + * + * + * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + * `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + * + * * Default: "Replace" */ action?: Action; /** * Modulus to take of the hash of the source label values. + * + * * Only applicable when the action is `HashMod`. */ modulus?: number; @@ -368,8 +496,10 @@ export interface MetricRelabeling { */ regex?: string; /** - * Replacement value against which a Replace action is performed if the regular expression - * matches. + * Replacement value against which a Replace action is performed if the + * regular expression matches. + * + * * Regex capture groups are available. */ replacement?: string; @@ -378,14 +508,19 @@ export interface MetricRelabeling { */ separator?: string; /** - * The source labels select values from existing labels. Their content is concatenated using - * the configured Separator and matched against the configured regular expression. + * The source labels select values from existing labels. Their content is + * concatenated using the configured Separator and matched against the + * configured regular expression. */ sourceLabels?: string[]; /** * Label to which the resulting string is written in a replacement. - * It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, `KeepEqual` and - * `DropEqual` actions. + * + * + * It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + * `KeepEqual` and `DropEqual` actions. + * + * * Regex capture groups are available. */ targetLabel?: string; @@ -393,8 +528,12 @@ export interface MetricRelabeling { /** * Action to perform based on the regex matching. - * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. `DropEqual` and - * `KeepEqual` actions require Prometheus >= v2.41.0. + * + * + * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + * `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + * + * * Default: "Replace" */ export enum Action { @@ -424,20 +563,27 @@ export enum Action { /** * `oauth2` configures the OAuth2 settings to use when scraping the target. + * + * * It requires Prometheus >= 2.27.0. + * + * * Cannot be set at the same time as `authorization`, or `basicAuth`. */ export interface Oauth2 { /** - * `clientId` specifies a key of a Secret or ConfigMap containing the OAuth2 client's ID. + * `clientId` specifies a key of a Secret or ConfigMap containing the + * OAuth2 client's ID. */ clientId: ClientID; /** - * `clientSecret` specifies a key of a Secret containing the OAuth2 client's secret. + * `clientSecret` specifies a key of a Secret containing the OAuth2 + * client's secret. */ clientSecret: ClientSecret; /** - * `endpointParams` configures the HTTP parameters to append to the token URL. + * `endpointParams` configures the HTTP parameters to append to the token + * URL. */ endpointParams?: { [key: string]: string }; /** @@ -451,7 +597,8 @@ export interface Oauth2 { } /** - * `clientId` specifies a key of a Secret or ConfigMap containing the OAuth2 client's ID. + * `clientId` specifies a key of a Secret or ConfigMap containing the + * OAuth2 client's ID. */ export interface ClientID { /** @@ -473,9 +620,14 @@ export interface ClientIDConfigMap { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -493,9 +645,14 @@ export interface ClientIDSecret { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -505,7 +662,8 @@ export interface ClientIDSecret { } /** - * `clientSecret` specifies a key of a Secret containing the OAuth2 client's secret. + * `clientSecret` specifies a key of a Secret containing the OAuth2 + * client's secret. */ export interface ClientSecret { /** @@ -513,9 +671,14 @@ export interface ClientSecret { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -525,21 +688,29 @@ export interface ClientSecret { } /** - * RelabelConfig allows dynamic rewriting of the label set for targets, alerts, scraped - * samples and remote write samples. + * RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + * scraped samples and remote write samples. + * + * * More info: * https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config */ export interface Relabeling { /** * Action to perform based on the regex matching. - * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. `DropEqual` and - * `KeepEqual` actions require Prometheus >= v2.41.0. + * + * + * `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + * `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + * + * * Default: "Replace" */ action?: Action; /** * Modulus to take of the hash of the source label values. + * + * * Only applicable when the action is `HashMod`. */ modulus?: number; @@ -548,8 +719,10 @@ export interface Relabeling { */ regex?: string; /** - * Replacement value against which a Replace action is performed if the regular expression - * matches. + * Replacement value against which a Replace action is performed if the + * regular expression matches. + * + * * Regex capture groups are available. */ replacement?: string; @@ -558,14 +731,19 @@ export interface Relabeling { */ separator?: string; /** - * The source labels select values from existing labels. Their content is concatenated using - * the configured Separator and matched against the configured regular expression. + * The source labels select values from existing labels. Their content is + * concatenated using the configured Separator and matched against the + * configured regular expression. */ sourceLabels?: string[]; /** * Label to which the resulting string is written in a replacement. - * It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, `KeepEqual` and - * `DropEqual` actions. + * + * + * It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + * `KeepEqual` and `DropEqual` actions. + * + * * Regex capture groups are available. */ targetLabel?: string; @@ -573,8 +751,12 @@ export interface Relabeling { /** * HTTP scheme to use for scraping. - * `http` and `https` are the expected values unless you rewrite the `__scheme__` label via - * relabeling. + * + * + * `http` and `https` are the expected values unless you rewrite the + * `__scheme__` label via relabeling. + * + * * If empty, Prometheus uses the default value `http`. */ export enum Scheme { @@ -643,9 +825,14 @@ export interface CAConfigMap { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -663,9 +850,14 @@ export interface CASecret { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -697,9 +889,14 @@ export interface CERTConfigMap { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -717,9 +914,14 @@ export interface CERTSecret { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -737,9 +939,14 @@ export interface KeySecret { */ key: string; /** - * Name of the referent. More info: - * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add - * other useful fields. apiVersion, kind, uid? + * Name of the referent. + * This field is effectively required, but due to backwards compatibility is + * allowed to be empty. Instances of this type with an empty value here are + * almost certainly wrong. + * TODO: Add other useful fields. apiVersion, kind, uid? + * More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * TODO: Drop `kubebuilder:default` when controller-gen doesn't need it + * https://github.com/kubernetes-sigs/kubebuilder/issues/3896. */ name?: string; /** @@ -749,13 +956,13 @@ export interface KeySecret { } /** - * Selector to select which namespaces the Kubernetes `Endpoints` objects are discovered - * from. + * Selector to select which namespaces the Kubernetes `Endpoints` objects + * are discovered from. */ export interface NamespaceSelector { /** - * Boolean describing whether all namespaces are selected in contrast to a list restricting - * them. + * Boolean describing whether all namespaces are selected in contrast to a + * list restricting them. */ any?: boolean; /** @@ -764,6 +971,21 @@ export interface NamespaceSelector { matchNames?: string[]; } +/** + * ScrapeProtocol represents a protocol used by Prometheus for scraping metrics. + * Supported values are: + * * `OpenMetricsText0.0.1` + * * `OpenMetricsText1.0.0` + * * `PrometheusProto` + * * `PrometheusText0.0.4` + */ +export enum ScrapeProtocol { + OpenMetricsText001 = "OpenMetricsText0.0.1", + OpenMetricsText100 = "OpenMetricsText1.0.0", + PrometheusProto = "PrometheusProto", + PrometheusText004 = "PrometheusText0.0.4", +} + /** * Label selector to select the Kubernetes `Endpoints` objects. */ @@ -773,16 +995,17 @@ export interface Selector { */ matchExpressions?: MatchExpression[]; /** - * matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is - * equivalent to an element of matchExpressions, whose key field is "key", the operator is - * "In", and the values array contains only "value". The requirements are ANDed. + * matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + * map is equivalent to an element of matchExpressions, whose key field is "key", the + * operator is "In", and the values array contains only "value". The requirements are ANDed. */ matchLabels?: { [key: string]: string }; } /** * A label selector requirement is a selector that contains values, a key, and an operator - * that relates the key and values. + * that + * relates the key and values. */ export interface MatchExpression { /** @@ -790,14 +1013,15 @@ export interface MatchExpression { */ key: string; /** - * operator represents a key's relationship to a set of values. Valid operators are In, - * NotIn, Exists and DoesNotExist. + * operator represents a key's relationship to a set of values. + * Valid operators are In, NotIn, Exists and DoesNotExist. */ operator: string; /** - * values is an array of string values. If the operator is In or NotIn, the values array - * must be non-empty. If the operator is Exists or DoesNotExist, the values array must be - * empty. This array is replaced during a strategic merge patch. + * values is an array of string values. If the operator is In or NotIn, + * the values array must be non-empty. If the operator is Exists or DoesNotExist, + * the values array must be empty. This array is replaced during a strategic + * merge patch. */ values?: string[]; } @@ -806,4 +1030,5 @@ RegisterKind(ServiceMonitor, { group: "monitoring.coreos.com", version: "v1", kind: "ServiceMonitor", + plural: "servicemonitors", }); diff --git a/src/pepr/operator/crd/index.ts b/src/pepr/operator/crd/index.ts index 163b8387a..d92e14d2a 100644 --- a/src/pepr/operator/crd/index.ts +++ b/src/pepr/operator/crd/index.ts @@ -2,8 +2,8 @@ export { Allow, Direction, Expose, - Monitor, Gateway, + Monitor, Phase, Status as PkgStatus, RemoteGenerated, @@ -20,17 +20,32 @@ export { } from "./generated/exemption-v1alpha1"; export { - VirtualService as IstioVirtualService, - HTTPRoute as IstioHTTPRoute, HTTP as IstioHTTP, + HTTPRoute as IstioHTTPRoute, + VirtualService as IstioVirtualService, } from "./generated/istio/virtualservice-v1beta1"; export { - ServiceEntry as IstioServiceEntry, - Location as IstioLocation, - Resolution as IstioResolution, Endpoint as IstioEndpoint, + Location as IstioLocation, Port as IstioPort, + Resolution as IstioResolution, + ServiceEntry as IstioServiceEntry, } from "./generated/istio/serviceentry-v1beta1"; -export * as Prometheus from "./generated/prometheus/servicemonitor-v1"; +export { + Scheme as PodMonitorScheme, + PodMonitor as PrometheusPodMonitor, +} from "./generated/prometheus/podmonitor-v1"; + +export { + ServiceMonitor as PrometheusServiceMonitor, + Endpoint as ServiceMonitorEndpoint, + Scheme as ServiceMonitorScheme, +} from "./generated/prometheus/servicemonitor-v1"; + +export { + Action as IstioAction, + AuthorizationPolicy as IstioAuthorizationPolicy, +} from "./generated/istio/authorizationpolicy-v1beta1"; +export { RequestAuthentication as IstioRequestAuthentication } from "./generated/istio/requestauthentication-v1"; diff --git a/src/pepr/operator/crd/sources/package/v1alpha1.ts b/src/pepr/operator/crd/sources/package/v1alpha1.ts index 0a61f8ae1..e5628b230 100644 --- a/src/pepr/operator/crd/sources/package/v1alpha1.ts +++ b/src/pepr/operator/crd/sources/package/v1alpha1.ts @@ -2,6 +2,40 @@ import { V1CustomResourceDefinitionVersion, V1JSONSchemaProps } from "@kubernete import { advancedHTTP } from "../istio/virtualservice-v1beta1"; +const AuthorizationSchema: V1JSONSchemaProps = { + description: "Authorization settings.", + type: "object", + properties: { + credentials: { + description: + "Selects a key of a Secret in the namespace that contains the credentials for authentication.", + type: "object", + properties: { + key: { + description: "The key of the secret to select from. Must be a valid secret key.", + type: "string", + }, + name: { + description: + "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + type: "string", + }, + optional: { + description: "Specify whether the Secret or its key must be defined", + type: "boolean", + }, + }, + required: ["key"], // Ensure key is required in the schema + }, + type: { + description: + 'Defines the authentication type. The value is case-insensitive. "Basic" is not a supported value. Default: "Bearer"', + type: "string", + }, + }, + required: ["credentials"], // Ensure credentials is required in the schema +}; + const allow = { description: "Allow specific traffic (namespace will have a default-deny policy)", type: "array", @@ -160,7 +194,7 @@ const expose = { } as V1JSONSchemaProps; const monitor = { - description: "Create Service Monitor configurations", + description: "Create Service or Pod Monitor configurations", type: "array", items: { type: "object", @@ -202,6 +236,13 @@ const monitor = { description: "HTTP path from which to scrape for metrics, defaults to `/metrics`", type: "string", }, + kind: { + description: + "The type of monitor to create; PodMonitor or ServiceMonitor. ServiceMonitor is the default.", + enum: ["PodMonitor", "ServiceMonitor"], + type: "string", + }, + authorization: AuthorizationSchema, }, }, }; @@ -213,10 +254,13 @@ const sso = { type: "object", required: ["clientId", "name", "redirectUris"], properties: { - isAuthSvcClient: { - description: "If true, the client will generate a new Auth Service client as well", - type: "boolean", - default: false, + enableAuthserviceSelector: { + description: + "Labels to match pods to automatically protect with authservice. Leave empty to disable authservice protection", + type: "object", + additionalProperties: { + type: "string", + }, }, secretName: { description: "The name of the secret to store the client secret", @@ -385,6 +429,12 @@ export const v1alpha1: V1CustomResourceDefinitionVersion = { type: "string", }, }, + authserviceClients: { + type: "array", + items: { + type: "string", + }, + }, endpoints: { type: "array", items: { diff --git a/src/pepr/operator/index.ts b/src/pepr/operator/index.ts index 0555c5ab2..5c11232a2 100644 --- a/src/pepr/operator/index.ts +++ b/src/pepr/operator/index.ts @@ -16,6 +16,7 @@ import { UDSExemption, UDSPackage } from "./crd"; import { validator } from "./crd/validators/package-validator"; // Reconciler imports +import { purgeAuthserviceClients } from "./controllers/keycloak/authservice/authservice"; import { exemptValidator } from "./crd/validators/exempt-validator"; import { packageReconciler } from "./reconcilers/package-reconciler"; @@ -49,6 +50,7 @@ When(UDSPackage) // Remove any SSO clients await purgeSSOClients(pkg, []); + await purgeAuthserviceClients(pkg, []); }); // Watch for changes to the UDSPackage CRD to enqueue a package for processing diff --git a/src/pepr/operator/reconcilers/package-reconciler.ts b/src/pepr/operator/reconcilers/package-reconciler.ts index e4062b294..0312441ed 100644 --- a/src/pepr/operator/reconcilers/package-reconciler.ts +++ b/src/pepr/operator/reconcilers/package-reconciler.ts @@ -3,7 +3,9 @@ import { UDSConfig } from "../../config"; import { Component, setupLogger } from "../../logger"; import { enableInjection } from "../controllers/istio/injection"; import { istioResources } from "../controllers/istio/istio-resources"; +import { authservice } from "../controllers/keycloak/authservice/authservice"; import { keycloak } from "../controllers/keycloak/client-sync"; +import { podMonitor } from "../controllers/monitoring/pod-monitor"; import { serviceMonitor } from "../controllers/monitoring/service-monitor"; import { networkPolicies } from "../controllers/network/policies"; import { Phase, UDSPackage } from "../crd"; @@ -46,24 +48,26 @@ export async function packageReconciler(pkg: UDSPackage) { // Update the namespace to ensure the istio-injection label is set await enableInjection(pkg); + // Configure SSO + const ssoClients = await keycloak(pkg); + const authserviceClients = await authservice(pkg, ssoClients); + // Create the VirtualService and ServiceEntry for each exposed service endpoints = await istioResources(pkg, namespace!); // Only configure the ServiceMonitors if not running in single test mode - let monitors: string[] = []; + const monitors: string[] = []; if (!UDSConfig.isSingleTest) { - // Create the ServiceMonitor for each monitored service - monitors = await serviceMonitor(pkg, namespace!); + monitors.push(...(await podMonitor(pkg, namespace!))); + monitors.push(...(await serviceMonitor(pkg, namespace!))); } else { - log.warn(`Running in single test mode, skipping ${name} ServiceMonitors.`); + log.warn(`Running in single test mode, skipping ${name} Monitors.`); } - // Configure SSO - const ssoClients = await keycloak(pkg); - await updateStatus(pkg, { phase: Phase.Ready, - ssoClients, + ssoClients: [...ssoClients.keys()], + authserviceClients, endpoints, monitors, networkPolicyCount: netPol.length, diff --git a/src/pepr/prometheus/index.ts b/src/pepr/prometheus/index.ts index cc8e022d4..04886748f 100644 --- a/src/pepr/prometheus/index.ts +++ b/src/pepr/prometheus/index.ts @@ -1,6 +1,10 @@ import { Capability, K8s, kind } from "pepr"; import { Component, setupLogger } from "../logger"; -import { Prometheus } from "../operator/crd"; +import { + PrometheusServiceMonitor, + ServiceMonitorEndpoint, + ServiceMonitorScheme, +} from "../operator/crd"; // configure subproject logger const log = setupLogger(Component.PROMETHEUS); @@ -15,11 +19,18 @@ const { When } = prometheus; /** * Mutate a service monitor to enable mTLS metrics */ -When(Prometheus.ServiceMonitor) +When(PrometheusServiceMonitor) .IsCreatedOrUpdated() .Mutate(async sm => { // Provide an opt-out of mutation to handle complicated scenarios if (sm.Raw.metadata?.annotations?.["uds/skip-sm-mutate"]) { + log.info( + `Mutating scrapeClass to exempt ServiceMonitor ${sm.Raw.metadata?.name} from default scrapeClass mTLS config`, + ); + if (sm.Raw.spec === undefined) { + return; + } + sm.Raw.spec.scrapeClass = "exempt"; return; } @@ -28,7 +39,10 @@ When(Prometheus.ServiceMonitor) if (sm.Raw.spec?.endpoints === undefined) { return; } - + /** + * Patching ServiceMonitor tlsConfig is deprecated in favor of default scrapeClass with tls config + * this mutation will be removed in favor of a mutation to opt-out of the default scrapeClass in the future + */ log.info(`Patching service monitor ${sm.Raw.metadata?.name} for mTLS metrics`); const tlsConfig = { caFile: "/etc/prom-certs/root-cert.pem", @@ -36,18 +50,24 @@ When(Prometheus.ServiceMonitor) keyFile: "/etc/prom-certs/key.pem", insecureSkipVerify: true, }; - const endpoints: Prometheus.Endpoint[] = sm.Raw.spec.endpoints; + const endpoints: ServiceMonitorEndpoint[] = sm.Raw.spec.endpoints; endpoints.forEach(endpoint => { - endpoint.scheme = Prometheus.Scheme.HTTPS; + endpoint.scheme = ServiceMonitorScheme.HTTPS; endpoint.tlsConfig = tlsConfig; }); sm.Raw.spec.endpoints = endpoints; } else { - log.info(`No mutations needed for service monitor ${sm.Raw.metadata?.name}`); + log.info( + `Mutating scrapeClass to exempt ServiceMonitor ${sm.Raw.metadata?.name} from default scrapeClass mTLS config`, + ); + if (sm.Raw.spec === undefined) { + return; + } + sm.Raw.spec.scrapeClass = "exempt"; } }); -async function isIstioInjected(sm: Prometheus.ServiceMonitor) { +async function isIstioInjected(sm: PrometheusServiceMonitor) { const namespaces = sm.Raw.spec?.namespaceSelector?.matchNames || [sm.Raw.metadata?.namespace] || [ "default", ]; diff --git a/src/prometheus-stack/chart/templates/istio-monitor.yaml b/src/prometheus-stack/chart/templates/istio-monitor.yaml index e82a0d23e..1311f4658 100644 --- a/src/prometheus-stack/chart/templates/istio-monitor.yaml +++ b/src/prometheus-stack/chart/templates/istio-monitor.yaml @@ -5,6 +5,7 @@ metadata: name: envoy-stats-monitor namespace: istio-system spec: + scrapeClass: exempt selector: matchExpressions: - {key: istio-prometheus-ignore, operator: DoesNotExist} diff --git a/src/prometheus-stack/chart/templates/prometheus-pod-monitor.yaml b/src/prometheus-stack/chart/templates/prometheus-pod-monitor.yaml index 51e17961d..60c3bb615 100644 --- a/src/prometheus-stack/chart/templates/prometheus-pod-monitor.yaml +++ b/src/prometheus-stack/chart/templates/prometheus-pod-monitor.yaml @@ -5,6 +5,7 @@ metadata: name: prometheus-pod-monitor namespace: monitoring spec: + scrapeClass: exempt selector: matchLabels: app: prometheus diff --git a/src/prometheus-stack/values/values.yaml b/src/prometheus-stack/values/values.yaml index 30d2b6559..fe6f21d26 100644 --- a/src/prometheus-stack/values/values.yaml +++ b/src/prometheus-stack/values/values.yaml @@ -24,6 +24,16 @@ prometheus: prometheusSpec: enableFeatures: - remote-write-receiver + additionalConfig: + scrapeClasses: + - name: istio-certs + default: true + tlsConfig: + caFile: /etc/prom-certs/root-cert.pem + certFile: /etc/prom-certs/cert-chain.pem + keyFile: /etc/prom-certs/key.pem + insecureSkipVerify: true + - name: exempt podMetadata: annotations: proxy.istio.io/config: | diff --git a/src/test/app-authservice-tenant.yaml b/src/test/app-authservice-tenant.yaml new file mode 100644 index 000000000..094bff22e --- /dev/null +++ b/src/test/app-authservice-tenant.yaml @@ -0,0 +1,84 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: authservice-test-app +--- +apiVersion: uds.dev/v1alpha1 +kind: Package +metadata: + name: httpbin-other + namespace: authservice-test-app +spec: + sso: + - name: Demo SSO + clientId: uds-core-httpbin + redirectUris: + - "https://protected.uds.dev/login" + enableAuthserviceSelector: + app: httpbin + groups: + anyOf: + - "/UDS Core/Admin" + network: + expose: + - service: httpbin + selector: + app: httpbin + gateway: tenant + host: protected + port: 8000 + targetPort: 80 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: httpbin + namespace: authservice-test-app +--- +apiVersion: v1 +kind: Service +metadata: + name: httpbin + namespace: authservice-test-app + labels: + app: httpbin + service: httpbin +spec: + ports: + - name: http + port: 8000 + targetPort: 80 + selector: + app: httpbin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: httpbin + namespace: authservice-test-app +spec: + replicas: 1 + selector: + matchLabels: + app: httpbin + version: v1 + template: + metadata: + labels: + app: httpbin + version: v1 + spec: + serviceAccountName: httpbin + containers: + - image: docker.io/kong/httpbin + imagePullPolicy: IfNotPresent + name: httpbin + resources: + limits: + cpu: 50m + memory: 64Mi + requests: + cpu: 50m + memory: 64Mi + ports: + - containerPort: 80 diff --git a/src/test/app-tenant.yaml b/src/test/app-tenant.yaml index 3eb203b99..a16e89349 100644 --- a/src/test/app-tenant.yaml +++ b/src/test/app-tenant.yaml @@ -3,6 +3,43 @@ kind: Namespace metadata: name: test-tenant-app --- +apiVersion: v1 +kind: Secret +metadata: + name: example-secret + namespace: test-tenant-app +type: Opaque +data: + example-key: ZXhhbXBsZS1rZXk= +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: httpbin-pod-monitor-default-scrape + namespace: test-tenant-app +spec: + podMetricsEndpoints: + - path: /metrics + port: service + scrapeClass: istio-certs + selector: + matchLabels: + app: httpbin +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: httpbin-pod-monitor-no-tls-config + namespace: test-tenant-app +spec: + podMetricsEndpoints: + - path: /metrics + port: service + scrapeClass: exempt + selector: + matchLabels: + app: httpbin +--- apiVersion: uds.dev/v1alpha1 kind: Package metadata: @@ -23,6 +60,47 @@ spec: gateway: tenant host: demo-8081 port: 8081 + monitor: + - selector: + app: httpbin + targetPort: 3000 + portName: service + description: Pod Monitor + kind: PodMonitor + - selector: + app: httpbin + targetPort: 3000 + portName: service + description: Service Monitor Explicit + kind: ServiceMonitor + - selector: + app: httpbin + targetPort: 3000 + portName: service + description: Service Monitor Default + - portName: "http" + selector: + app: "example" + targetPort: 8080 + kind: "PodMonitor" + authorization: + credentials: + key: "example-key" + name: "example-secret" + optional: false + type: "Bearer" + description: Pod Monitor with Authorization + - portName: "http" + selector: + app: "example" + targetPort: 8080 + authorization: + credentials: + key: "example-key" + name: "example-secret" + optional: false + type: "Bearer" + description: Service Monitor with Authorization --- apiVersion: v1 kind: Service diff --git a/src/test/tasks.yaml b/src/test/tasks.yaml index c4ab3d4c2..18bd46153 100644 --- a/src/test/tasks.yaml +++ b/src/test/tasks.yaml @@ -57,6 +57,37 @@ tasks: address: demo-8081.uds.dev code: 200 + - description: Verify the authservice tenant app is accessible + wait: + network: + protocol: https + address: protected.uds.dev + code: 200 + + - description: Wait for authservice to reload configuration + wait: + cluster: + kind: Deployment + name: authservice + namespace: authservice + + - description: Verify the authservice tenant app is protected by checking redirect + maxRetries: 3 + cmd: | + set -e + SSO_REDIRECT=$(uds zarf tools kubectl run curl-test --image=cgr.dev/chainguard/curl:latest -q --restart=Never --rm -i -- -Ls -o /dev/null -w %{url_effective} "https://protected.uds.dev") + + case "${SSO_REDIRECT}" in + "https://sso.uds.dev"*) + echo "Protected by authservice" + ;; + *) + # Fallback option if the condition is false + echo "App is not protected by authservice" + echo $SSO_REDIRECT + exit 1 + ;; + esac - description: Verify podinfo is healthy wait: cluster: diff --git a/src/test/zarf.yaml b/src/test/zarf.yaml index 725d5baa2..e38657758 100644 --- a/src/test/zarf.yaml +++ b/src/test/zarf.yaml @@ -14,6 +14,9 @@ components: - name: app-tenant files: - "app-tenant.yaml" + - name: app-authservice-tenant + files: + - "app-authservice-tenant.yaml" images: - docker.io/kong/httpbin:latest - hashicorp/http-echo:latest diff --git a/tasks.yaml b/tasks.yaml index a517d5550..1cbb13b9e 100644 --- a/tasks.yaml +++ b/tasks.yaml @@ -22,6 +22,7 @@ tasks: task: test-uds-core - name: dev-setup + description: "Create k3d cluster with istio" actions: - description: "Create the dev cluster" task: setup:create-k3d-cluster @@ -51,24 +52,42 @@ tasks: - description: "Deploy slim dev bundle" task: deploy:k3d-slim-dev-bundle + - name: dev-identity + description: "Create k3d cluster with istio, Pepr, Keycloak, and Authservice for development" + actions: + - task: dev-setup + + - description: "Deploy Pepr" + cmd: "npx pepr deploy --confirm" + + - description: "Deploy Keycloak" + cmd: "uds run dev-deploy --set PKG=keycloak" + + - description: "Deploy Authservice" + cmd: "uds run dev-deploy --set PKG=authservice" + - name: dev-deploy + description: "Deploy the given source package with Zarf Dev" actions: - - description: "Deploy the given source package with Zarf Dev" - cmd: "uds zarf dev deploy src/${PKG} --flavor ${FLAVOR}" + - cmd: "uds zarf dev deploy src/${PKG} --flavor ${FLAVOR}" - name: setup-cluster + description: "Create a k3d Cluster and Initialize with Zarf" actions: - task: setup:k3d-test-cluster - name: create-single-package + description: "Create a single Zarf Package, must set UDS_PKG environment variable" actions: - task: create:single-package - name: create-standard-package + description: "Create UDS Core Zarf Package, `upstream` flavor default, use --set FLAVOR={flavor} to change" actions: - task: create:standard-package - name: deploy-single-package + description: "Deploy Pepr Module and a Zarf Package using UDS_PKG environment variable" actions: - task: deploy:single-package @@ -77,14 +96,17 @@ tasks: - task: deploy:k3d-standard-bundle - name: test-single-package + description: "Build and test a single package, must set UDS_PKG environment variable" actions: - task: test:single-package - name: test-uds-core + description: "Build and test UDS Core" actions: - task: test:uds-core - name: test-uds-core-upgrade + description: "Test an upgrade from the latest released UDS Core package to current branch" actions: - task: test:uds-core-upgrade @@ -97,10 +119,12 @@ tasks: - task: test:compliance-evaluate - name: lint-check + description: "Run linting checks" actions: - task: lint:check - name: lint-fix + description: "Fix linting issues" actions: - task: lint:fix