diff --git a/kubernetes/apps/uptime-monitor/crd-endpointmonitors.yaml b/kubernetes/apps/uptime-monitor/crd-endpointmonitors.yaml new file mode 100644 index 00000000..7bf8128f --- /dev/null +++ b/kubernetes/apps/uptime-monitor/crd-endpointmonitors.yaml @@ -0,0 +1,464 @@ +# VENDORED, DO NOT EDIT BY HAND. +# +# Source: stakater/IngressMonitorController chart 2.2.13, +# charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml +# Copied verbatim at commit aaffd8a866aa09a77030979d938e766895bde5d4. +# +# Vendored rather than installed via HelmRelease because this Kustomization +# ships hand-written RBAC (see rbac.yaml) — the chart's own Role grants pods, +# services and `secrets: list`, none of which this controller uses. Pulling the +# chart for the CRD alone would drag those back in. +# +# To update: bump the image tag in deployment.yaml and re-copy this file from +# the matching chart tag. Nothing here is Renovate-managed, so the tag and this +# file move together, by hand, in one reviewable commit. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: endpointmonitors.endpointmonitor.stakater.com +spec: + group: endpointmonitor.stakater.com + names: + kind: EndpointMonitor + listKind: EndpointMonitorList + plural: endpointmonitors + singular: endpointmonitor + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: EndpointMonitor is the Schema for the endpointmonitors API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: EndpointMonitorSpec defines the desired state of EndpointMonitor + properties: + appInsightsConfig: + description: Configuration for AppInsights Monitor Provider + properties: + frequency: + description: 'Sets how often the test should run from each test + location. Possible values: `300,600,900` seconds' + type: integer + retryEnable: + description: 'If its `true`, falied test will be retry after a + short interval. Possible values: `true, false`' + type: boolean + statusCode: + description: Returned status code that is counted as a success + type: integer + type: object + forceHttps: + description: Force monitor endpoint to use HTTPS + type: boolean + gcloudConfig: + description: Configuration for Google Cloud Monitor Provider + properties: + projectId: + description: Google Cloud Project ID + type: string + type: object + grafanaConfig: + description: Configuration for Grafana Cloud Monitor Provider + properties: + alertSensitivity: + default: none + description: |- + The alertSensitivity value defaults to none if there are no alerts or can be set to low, medium, + or high to correspond to the check alert levels. + enum: + - none + - low + - medium + - high + type: string + frequency: + description: The frequency value specifies how often the check + runs in milliseconds + format: int64 + type: integer + probes: + description: |- + Probes are the monitoring agents responsible for simulating user interactions with your web applications + or services. These agents periodically send requests to predefined URLs and record the responses, + checking for expected outcomes and measuring performance. + items: + type: string + type: array + tenantId: + format: int64 + type: integer + type: object + healthEndpoint: + type: string + pingdomConfig: + description: Configuration for Pingdom Monitor Provider + properties: + alertContacts: + description: '`-` separated contact id''s (e.g. "1234567_8_9-9876543_2_1")' + type: string + alertIntegrations: + description: '`-` separated set list of integrations ids (e.g. + "91166-12168")' + type: string + basicAuthUser: + description: Required for basic-authentication + type: string + notifyWhenBackUp: + description: Set to "false" to disable recovery notifications + type: boolean + paused: + description: Set to "true" to pause checks + type: boolean + postDataEnvVar: + description: |- + Data that should be posted to the web page, for example submission data for a sign-up or login form. + The data needs to be formatted in the same way as a web browser would send it to the web server. + Because post data contains sensitive secret this field is only a reference to an environment variable. + type: string + requestHeaders: + description: Custom request headers + type: string + requestHeadersEnvVar: + description: |- + Custom request headers that should be read from an environment variable as it possibly contains sensitive data. + An example would be an API token. + type: string + resolution: + description: The pingdom check interval in minutes + type: integer + sendNotificationWhenDown: + description: How many failed check attempts before notifying + type: integer + shouldContain: + description: Set to text string that has to be present in the + HTML code of the page + type: string + sslDownDaysBefore: + description: |- + Consider down prior to certificate expiring + Select the number of days prior to your certificate expiry date that you want to consider the check down. + At this day your check will be considered down and if applicable a down alert will be sent. + type: integer + tags: + description: Comma separated set of tags to apply to check (e.g. + "testing,aws") + type: string + teamAlertContacts: + description: '`-` separated team id''s (e.g. "1234567_8_9-9876543_2_1")' + type: string + verifyCertificate: + description: |- + Monitor SSL/TLS certificate + Monitor the validity of your SSL/TLS certificate. With this enabled Uptime checks will be considered DOWN when + the certificate becomes invalid or expires. + SSL/TLS certificate monitoring is available for HTTP checks. + type: boolean + type: object + pingdomTransactionConfig: + description: Configuration for Pingdom Transaction Monitor Provider + properties: + alertContacts: + description: '`-` separated contact id''s (e.g. "1234567_8_9-9876543_2_1")' + type: string + alertIntegrations: + description: '`-` separated set list of integrations ids (e.g. + "91166-12168")' + type: string + custom_message: + description: Custom message that is part of the email and webhook + alerts + type: string + interval: + description: 'TMS test intervals in minutes. Allowed intervals: + 5,10,20,60,720,1440. The interval you''re allowed to set may + vary depending on your current plan.' + enum: + - 5 + - 10 + - 20 + - 60 + - 720 + - 1440 + type: integer + paused: + description: 'Check status: active or inactive' + type: boolean + region: + description: 'Name of the region where the check is executed. + Supported regions: us-east, us-west, eu, au' + enum: + - us-east + - us-west + - eu + - au + type: string + send_notification_when_down: + description: Send notification when down X times + format: int64 + type: integer + severity_level: + description: 'Check importance- how important are the alerts when + the check fails. Allowed values: low, high' + enum: + - low + - high + type: string + steps: + description: steps to be executed as part of the check + items: + description: PingdomStep respresents a step of the script to + run a transcaction check + properties: + args: + additionalProperties: + type: string + description: |- + contains the html element with assigned value + the key element is always lowercase for example {"url": "https://www.pingdom.com"} + see available values at https://pkg.go.dev/github.com/karlderkaefer/pingdom-golang-client@latest/pkg/pingdom/client/tmschecks#StepArg + type: object + function: + description: |- + contains the function that is executed as part of the step + commands: go_to, click, fill, check, uncheck, sleep, select_radio, basic_auth, submit, wait_for_element, wait_for_contains + validations: url, exists, not_exists, contains, not_contains, field_contains, field_not_contains, is_checked, is_not_checked, radio_selected, dropdown_selected, dropdown_not_selected + see updated list https://docs.pingdom.com/api/#section/TMS-Steps-Vocabulary/Script-transaction-checks + type: string + required: + - args + - function + type: object + type: array + tags: + description: List of tags for a check. The tag name may contain + the characters 'A-Z', 'a-z', '0-9', '_' and '-'. The maximum + length of a tag is 64 characters. + items: + type: string + type: array + teamAlertContacts: + description: '`-` separated team id''s (e.g. "1234567_8_9-9876543_2_1")' + type: string + required: + - steps + type: object + providers: + description: Comma separated list of providers + type: string + statusCakeConfig: + description: Configuration for StatusCake Monitor Provider + properties: + basicAuthSecret: + description: Basic Auth Secret Name + type: string + basicAuthUser: + description: Basic Auth User + type: string + checkRate: + default: 300 + description: Set Check Rate for the monitor. + type: integer + confirmation: + description: Confirmation value ranges from (0,10) + maximum: 10 + minimum: 0 + type: integer + contactGroup: + description: Contact Group to be alerted. + type: string + enableSslAlert: + description: Enable SSL Alert + type: boolean + findString: + description: String to look for within the response. Considered + down if not found + type: string + followRedirect: + description: Enable ingress redirects + type: boolean + paused: + description: Pause the service + type: boolean + pingUrl: + description: Webhook for alerts + type: string + port: + description: TCP Port + type: integer + rawPostData: + description: RawPostData can be used to send parameters within + the URL. Changes the request from a GET to a POST + type: string + realBrowser: + description: Enable Real Browser + type: boolean + regions: + description: Comma separated list of Node Location IDs + type: string + statusCodes: + description: Comma separated list of HTTP codes to trigger error + on + type: string + testTags: + description: Comma separated list of tags + type: string + testType: + description: Set Test type - HTTP, TCP, PING, or Heartbeat + enum: + - HTTP + - TCP + - PING + - Heartbeat + type: string + timeout: + description: Timeout is used to set a user agent string. + maximum: 75 + minimum: 5 + type: integer + triggerRate: + description: Minutes to wait before sending an alert + type: integer + userAgent: + description: UserAgent is used to set a user agent string. + type: string + type: object + x-kubernetes-validations: + - message: checkRate must be between 30 and 172800 seconds for Heartbeat + monitors + rule: self.testType != 'Heartbeat' || (self.checkRate >= 30 && self.checkRate + <= 172800) + - message: 'checkRate for uptime monitors must be one of: 0, 30, 60, + 300, 900, 1800, 3600, 86400' + rule: self.testType == 'Heartbeat' || self.checkRate in [0, 30, + 60, 300, 900, 1800, 3600, 86400] + updownConfig: + description: Configuration for Updown Monitor Provider + properties: + enable: + description: Enable or disable checks + type: boolean + period: + description: The pingdom check interval in seconds + type: integer + publishPage: + description: Make status page public or not + type: boolean + requestHeaders: + description: Additional request headers for API calls + type: string + type: object + uptimeConfig: + description: Configuration for Uptime Monitor Provider + properties: + checkType: + description: The uptime check type that can be HTTP/DNS/ICMP etc. + type: string + contacts: + description: Add one or more contact groups separated by `,` + type: string + interval: + description: The uptime check interval in seconds + type: integer + locations: + description: Add different locations for the check + type: string + tags: + description: Add one or more tags for the check separated by `,` + type: string + type: object + uptimeRobotConfig: + description: Configuration for UptimeRobot Monitor Provider + properties: + alertContacts: + description: The uptimerobot alertContacts to be associated with + this monitor + type: string + customHTTPStatuses: + description: |- + Defines which http status codes are treated as up or down + For ex: 200:0_401:1_503:1 (to accept 200 as down and 401 and 503 as up) + type: string + interval: + description: The uptimerobot check interval in seconds + minimum: 60 + type: integer + keywordExists: + description: Alert if value exist (yes) or doesn't exist (no) + (Only if monitor-type is keyword) + enum: + - "yes" + - "no" + type: string + keywordValue: + description: keyword to check on URL (e.g.'search' or '404') (Only + if monitor-type is keyword) + type: string + maintenanceWindows: + description: Specify maintenanceWindows i.e. once or recurring + “do-not-monitor periods” + type: string + monitorType: + description: The uptimerobot monitor type (http or keyword) + enum: + - http + - keyword + type: string + statusPages: + description: The uptimerobot public status page ID to add this + monitor to + type: string + type: object + url: + description: URL to monitor + type: string + urlFrom: + description: URL to monitor from either an ingress or route reference + properties: + ingressRef: + description: IngressURLSource selects an Ingress to populate the + URL with + properties: + name: + type: string + required: + - name + type: object + routeRef: + description: RouteURLSource selects a Route to populate the URL + with + properties: + name: + type: string + required: + - name + type: object + type: object + type: object + status: + description: EndpointMonitorStatus defines the observed state of EndpointMonitor + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/kubernetes/apps/uptime-monitor/deployment.yaml b/kubernetes/apps/uptime-monitor/deployment.yaml new file mode 100644 index 00000000..7045e798 --- /dev/null +++ b/kubernetes/apps/uptime-monitor/deployment.yaml @@ -0,0 +1,118 @@ +--- +# Stakater IngressMonitorController (REN-6). +# +# It ships with zero EndpointMonitor resources, which is the whole point of this +# first deployment: the reconciler is keyed on one custom resource per event +# (endpointmonitor_controller.go:62-107) and never enumerates the UptimeRobot +# account, so with no EndpointMonitors it reconciles nothing and makes no API +# call to UptimeRobot at all. The 9 monitors already in that account are not +# read, not updated and not deleted. +# +# Plain manifests, not the upstream HelmRelease: the chart's RBAC grants pods, +# services and `secrets: list`, and it also creates a Service and optional +# cert-manager objects for a metrics endpoint that is disabled below. The +# maintained thing we depend on is the upstream image, which is pinned here. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingressmonitorcontroller + namespace: uptime-monitor + labels: + app.kubernetes.io/name: ingressmonitorcontroller + annotations: + # The API key is read ONCE, at startup (pkg/config/config.go + # `LoadControllerConfig` runs before the manager starts), so a rotated + # Secret is not picked up until the pod restarts. Reloader — which this + # cluster already runs — does that restart. Same idiom as + # apps/external-dns/helmrelease.yaml. + secret.reloader.stakater.com/reload: uptime-robot-api-key +spec: + replicas: 1 + strategy: + # Recreate, not RollingUpdate, and no --leader-elect. A rolling update would + # briefly run two reconcilers against the same UptimeRobot account; Recreate + # makes "at most one controller pod, ever" a property of the Deployment + # instead of something a Lease has to arbitrate. It costs a few seconds of + # controller downtime per upgrade, which for a monitor-syncing controller is + # nothing, and it removes the leases/events RBAC entirely. + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: ingressmonitorcontroller + template: + metadata: + labels: + app.kubernetes.io/name: ingressmonitorcontroller + spec: + serviceAccountName: ingressmonitorcontroller + automountServiceAccountToken: true + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: manager + # Pinned, and deliberately NOT under Flux image automation. rbac.yaml + # is derived from what this exact version calls; an automatic bump + # could add a call the Role does not cover, silently. + image: ghcr.io/stakater/ingressmonitorcontroller:v2.2.13 + command: ["/manager"] + args: + - --health-probe-bind-address=:8081 + # Metrics off. Enabling them means either TLS certs or + # `--metrics-secure=false`, and the authn/authz filter + # (cmd/main.go, `filters.WithAuthenticationAndAuthorization`) wants a + # ClusterRole for TokenReview/SubjectAccessReview — a cluster-scoped + # grant this namespace otherwise does not need. A ServiceMonitor for + # this controller is worth having, but it is its own change. + - --metrics-bind-address=0 + env: + # Scopes the manager's cache to this namespace + # (cmd/main.go `buildDefaultNamespaces`), which is what makes the + # namespaced Role in rbac.yaml sufficient — there is no ClusterRole. + - name: WATCH_NAMESPACE + value: uptime-monitor + # The namespace the config Secret is looked up in. Read from the + # ServiceAccount token file when unset; set explicitly so the lookup + # is visible in the manifest. + - name: OPERATOR_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + # IMC reads its whole configuration from key `config.yaml` of this + # Secret — providers, API key and enableMonitorDeletion together. + # ESO renders that key; see externalsecret.yaml for the contents and + # for why the config lives in git while the key does not. + - name: CONFIG_SECRET_NAME + value: uptime-robot-api-key + # Both probes are served by the manager on :8081 and are dialled by + # the kubelet, which Cilium identifies as the `host` entity — the only + # ingress the namespace policy admits + # (infrastructure/network-policies/uptime-monitor-ns.yaml). The port is + # not exposed as a containerPort because nothing else may reach it. + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + terminationGracePeriodSeconds: 10 diff --git a/kubernetes/apps/uptime-monitor/externalsecret.yaml b/kubernetes/apps/uptime-monitor/externalsecret.yaml index b9432d5e..c01c0c5d 100644 --- a/kubernetes/apps/uptime-monitor/externalsecret.yaml +++ b/kubernetes/apps/uptime-monitor/externalsecret.yaml @@ -12,12 +12,45 @@ # refreshInterval 1h matches the un-rotated ExternalSecrets here (velero, # wiki-*). The rotated ones use 10m so an automated roll propagates fast — if # kubernetes/infrastructure/openbao/rotation/ ever gains an uptime-robot -# rotator, drop this to 10m and annotate the controller Deployment for -# stakater/reloader, which the cluster already runs. +# rotator, drop this to 10m. The controller Deployment already carries the +# stakater/reloader annotation, so a new value restarts the pod that has the +# old one cached. # # The `eso` role already reads this path: eso-read grants `homelab/data/*` # (kubernetes/infrastructure/openbao/policies/eso-read.hcl:31). No OpenBao # policy change is needed for this key. +# +# --- +# Why this Secret holds a rendered `config.yaml` rather than a bare key (REN-6): +# IngressMonitorController does not read an API key from an env var. It reads +# its ENTIRE configuration — provider list, API key, enableMonitorDeletion — +# from one Secret key named `config.yaml` (pkg/config/config.go, +# `IngressMonitorControllerSecretConfigKey`), at startup, through the manager's +# APIReader. So the choice is not "key or config"; it is which Secret the config +# lives in. Templating it here keeps every non-secret decision in git and in +# review, with only the key itself injected at sync time. +# +# The template replaces the Secret's data rather than merging, so `apiKey` is a +# template input and the rendered `config.yaml` is the single output key. +# +# Two values are load-bearing: +# +# apiURL — REQUIRED. `Setup()` assigns `monitor.url = p.ApiURL` with no +# default (pkg/monitors/uptimerobot/uptime-monitor.go:39-44); omit it and +# every request goes to a relative URL. +# +# enableMonitorDeletion — true, so that removing an EndpointMonitor removes +# its monitor, which is what the mission asks for. It is bounded: deletion is +# reachable only via `GetByName`, which filters `m.FriendlyName == name` — +# exact equality — against the name the controller itself computed from the +# default template `{{.Name}}-{{.Namespace}}`. Every name it can compute ends +# in `-uptime-monitor`; none of the 9 monitors already in the account does. +# Set it to false for a create/update-only kill switch. +# +# monitorNameTemplate is deliberately absent: an empty value makes +# GetNameTemplateFormat fall back to that same default (pkg/util/template-utils.go:9), +# and writing it out here would mean escaping Go template syntax inside a Go +# template for no gain. apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: @@ -30,8 +63,17 @@ spec: name: openbao target: name: uptime-robot-api-key + template: + engineVersion: v2 + data: + config.yaml: | + providers: + - name: UptimeRobot + apiKey: "{{ .apiKey }}" + apiURL: https://api.uptimerobot.com/v2/ + enableMonitorDeletion: true data: - - secretKey: api-key + - secretKey: apiKey remoteRef: key: infrastructure/uptime-robot property: api_key diff --git a/kubernetes/apps/uptime-monitor/kustomization.yaml b/kubernetes/apps/uptime-monitor/kustomization.yaml index 4da8ca01..f1faeae3 100644 --- a/kubernetes/apps/uptime-monitor/kustomization.yaml +++ b/kubernetes/apps/uptime-monitor/kustomization.yaml @@ -1,4 +1,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: + - crd-endpointmonitors.yaml - externalsecret.yaml + - rbac.yaml + - deployment.yaml diff --git a/kubernetes/apps/uptime-monitor/rbac.yaml b/kubernetes/apps/uptime-monitor/rbac.yaml new file mode 100644 index 00000000..79203cfd --- /dev/null +++ b/kubernetes/apps/uptime-monitor/rbac.yaml @@ -0,0 +1,86 @@ +--- +# ServiceAccount + namespaced RBAC for IngressMonitorController (REN-6). +# +# There is NO ClusterRole here, on purpose. The Deployment sets +# WATCH_NAMESPACE=uptime-monitor, which makes the manager build a +# namespace-scoped cache (cmd/main.go `buildDefaultNamespaces`), so every read +# the controller performs is confined to this namespace. A ClusterRole would +# grant reach the process cannot use. +# +# The rules below were derived from what the pinned image actually calls, not +# from the chart's rules or the upstream kubebuilder markers. Deliberately NOT +# granted, and why: +# +# - `""/pods`, `""/services` — read from exactly one place, +# `tryGetHealthEndpointFromIngress` (pkg/kube/wrappers/ingress-wrapper.go:126), +# which is reached only from the `urlFrom.ingressRef` path and only when the +# EndpointMonitor sets no explicit `healthEndpoint`. It is a best-effort +# guess at a readiness-probe path: both the Get and the List log the error +# and return `"", false` on failure, so a denial costs a log line, not a +# reconcile. Since the ingressRef path can only reach this namespace anyway +# (see the ingresses rule below) and nothing here serves HTTP, the grant +# would be inert. Add it if an Ingress ever lands in `uptime-monitor`. +# - `route.openshift.io/routes` — OpenShift only; `kube.IsOpenshift` is false +# here, so the Route scheme is never even registered (cmd/main.go init()). +# - `endpointmonitors/finalizers` — IMC uses no finalizers. Deletion is driven +# by the reconciler's IsNotFound branch, not by a finalizer +# (endpointmonitor_controller.go:74-81); `grep -rn Finalizer internal/controller/` +# has no hits. +# - leases / events — no leader election (see the Recreate strategy comment in +# deployment.yaml) and the reconciler holds no EventRecorder. +# +# Re-check this list whenever the image tag in deployment.yaml moves: it is +# pinned and not Renovate-managed precisely so that never happens silently. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ingressmonitorcontroller + namespace: uptime-monitor +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ingressmonitorcontroller + namespace: uptime-monitor +rules: + # Its own custom resource — the only thing the reconciler is keyed on. + - apiGroups: ["endpointmonitor.stakater.com"] + resources: ["endpointmonitors"] + verbs: ["get", "list", "watch"] + # Not exercised by v2.2.13 (`grep -rn "Status()" internal/controller/` is + # empty), but granted: it is this CRD's own subresource, so it confers no + # reach outside the resource already listed above, and it is the one write an + # operator of this shape is expected to gain. + - apiGroups: ["endpointmonitor.stakater.com"] + resources: ["endpointmonitors/status"] + verbs: ["get", "update", "patch"] + # Read-only. Used to resolve an EndpointMonitor's URL from an Ingress via + # `spec.urlFrom.ingressRef`. Note that pkg/kube/util/url.go:71 resolves that + # ref against the EndpointMonitor's OWN namespace, so this grant can only ever + # reach Ingresses in `uptime-monitor` — it cannot read an app's Ingress. + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch"] + # The controller loads its entire configuration — including the UptimeRobot + # API key — from one Secret key at startup (pkg/config/config.go + # `LoadControllerConfig`). It reads it through the manager's APIReader, i.e. a + # direct GET by name and not a cached list, which is why `get` alone is enough + # and why resourceNames can pin it to the single Secret ESO renders. + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["uptime-robot-api-key"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ingressmonitorcontroller + namespace: uptime-monitor +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingressmonitorcontroller +subjects: + - kind: ServiceAccount + name: ingressmonitorcontroller + namespace: uptime-monitor diff --git a/kubernetes/clusters/homelab/uptime-monitor.yaml b/kubernetes/clusters/homelab/uptime-monitor.yaml index 395c29cd..30086958 100644 --- a/kubernetes/clusters/homelab/uptime-monitor.yaml +++ b/kubernetes/clusters/homelab/uptime-monitor.yaml @@ -1,7 +1,11 @@ # Automated uptime monitoring (REN-2): Stakater IngressMonitorController keeps -# UptimeRobot monitors in sync with `monitor.stakater.com/enabled` annotations on -# Ingress objects. This Kustomization starts with the API key only; the -# controller, its ServiceAccount and its RBAC land here in REN-6. +# UptimeRobot monitors in sync with `EndpointMonitor` custom resources. (Not +# Ingress annotations — `monitor.stakater.com/enabled` does nothing in IMC v2; +# there is no Ingress reconciler in the source, and docs/migration-guide.md +# calls the annotation mechanism obsolete. The opt-in property survives intact: +# creating an EndpointMonitor per host is more explicit than an annotation, and +# the default state is still nothing-monitored.) This Kustomization holds the +# API key, the controller Deployment, its ServiceAccount and its RBAC (REN-6). # # A DEDICATED Kustomization rather than adding the ExternalSecret to # ./kubernetes/infrastructure/post-crds, which is where the other @@ -36,6 +40,22 @@ # holds this Kustomization NotReady, and it is safe to fail loudly here # precisely because the blast radius is this Kustomization alone: nothing # `dependsOn` it. +# +# That gate earns its keep now that the controller is here (REN-6) rather than +# only the key. IMC GETs its whole configuration from the Secret once, at +# startup, through the manager's APIReader, and never revalidates. Which failure +# it papers over depends on which one happens: +# +# * Secret absent (first sync never succeeded — `creationPolicy` is the default +# Owner, so no successful sync means no Secret at all): IMC panics in +# `LoadControllerConfig` and the pod CrashLoopBackOffs. That one is loud. +# * Secret present but stale, because a LATER refresh failed: the pod starts +# against the old value, reports Available, and the first sign of trouble is +# a rejected UptimeRobot call on some future reconcile. Nothing about +# Deployment health says the key is current. +# +# The second case is the live one — the Secret exists today — and the CEL rule +# below is what turns it into a NotReady Kustomization instead of a green one. apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: @@ -43,9 +63,10 @@ metadata: namespace: flux-system spec: interval: 10m - # 2m, not the 1m most app Kustomizations use: the one object here is an - # ExternalSecret against a key a human may still be placing in OpenBao, and a - # retry every minute against a 404 is just noise in the ESO and Flux logs. + # 2m, not the 1m most app Kustomizations use: what stalls this Kustomization + # is an ExternalSecret against a key a human may still be placing in OpenBao, + # and a retry every minute against a 404 is just noise in the ESO and Flux + # logs. retryInterval: 2m timeout: 5m sourceRef: @@ -55,10 +76,10 @@ spec: prune: true wait: true # No name/namespace qualifier, so this is evaluated against every - # ExternalSecret in this Kustomization's inventory. Today that is exactly one - # (apps/uptime-monitor/externalsecret.yaml); when the IMC Deployment joins in - # REN-6 it is still one, and any ExternalSecret added here later should be held - # to the same bar anyway. + # ExternalSecret in this Kustomization's inventory. That is exactly one + # (apps/uptime-monitor/externalsecret.yaml) — the IMC Deployment added in + # REN-6 brought no second one — and any ExternalSecret added here later should + # be held to the same bar anyway. # # `external-secrets.io/v1` is the version every one of the 54 ExternalSecret # manifests in this repo is written against, so it is the GVK Flux records in diff --git a/kubernetes/infrastructure/namespaces/namespaces.yaml b/kubernetes/infrastructure/namespaces/namespaces.yaml index e8802f34..0ebd9443 100644 --- a/kubernetes/infrastructure/namespaces/namespaces.yaml +++ b/kubernetes/infrastructure/namespaces/namespaces.yaml @@ -245,14 +245,14 @@ kind: Namespace metadata: name: paperclip --- -# IngressMonitorController, which syncs UptimeRobot monitors from Ingress -# annotations (REN-2). Declared here rather than in the workload Kustomization +# IngressMonitorController, which syncs UptimeRobot monitors from +# `EndpointMonitor` custom resources (REN-2; not Ingress annotations — those are +# obsolete in IMC v2). Declared here rather than in the workload Kustomization # for the reason spelled out above `woodpecker`: the `infrastructure` KS creates # namespaces, and `infrastructure-crds` (post-crds, which includes # ../network-policies/) runs after it — so declaring it in the workload KS would -# invert that order the moment this namespace gains a CiliumNetworkPolicy, which -# it will when the controller arrives and needs scoped egress to the UptimeRobot -# API. It holds nothing but the API key Secret until then. +# invert that order the moment this namespace gains a CiliumNetworkPolicy. It +# has one now: ../network-policies/uptime-monitor-ns.yaml. # # No pod-security labels: the controller needs no hostPath, hostNetwork or # privilege, so the cluster's default `baseline` enforcement is enough. diff --git a/kubernetes/infrastructure/network-policies/kustomization.yaml b/kubernetes/infrastructure/network-policies/kustomization.yaml index f6589bc2..d1720901 100644 --- a/kubernetes/infrastructure/network-policies/kustomization.yaml +++ b/kubernetes/infrastructure/network-policies/kustomization.yaml @@ -29,4 +29,5 @@ resources: - wordpress-ns.yaml - ai-ns.yaml - navidrome-ns.yaml + - uptime-monitor-ns.yaml - allow-cilium-health.yaml diff --git a/kubernetes/infrastructure/network-policies/uptime-monitor-ns.yaml b/kubernetes/infrastructure/network-policies/uptime-monitor-ns.yaml new file mode 100644 index 00000000..5379e6da --- /dev/null +++ b/kubernetes/infrastructure/network-policies/uptime-monitor-ns.yaml @@ -0,0 +1,54 @@ +--- +# Baseline policy for the `uptime-monitor` namespace (REN-6). +# +# Copied from netbird-service-sync-ns.yaml, which is the closest existing +# neighbour in this directory: a controller that serves nothing and only calls +# out — kube-apiserver plus one external HTTPS API. Same shape here. +# +# No Traefik ingress rule, because nothing in this namespace is behind an +# Ingress. (For anyone reaching for a wiki note: the selector to admit Traefik +# lives in `ingress` with label `app.kubernetes.io/name: traefik`, as in +# paperclip-ns.yaml. An `ingress-nginx` selector matches nothing in this cluster +# and would admit nothing, silently.) +# +# Deliberately no OpenBao egress: the controller never talks to OpenBao. ESO, in +# the external-secrets namespace, resolves the key and writes the Secret; the +# controller only reads that Secret through the API server. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: default-deny + namespace: uptime-monitor +spec: + endpointSelector: {} + ingress: + # Kubelet liveness/readiness probes on :8081 arrive with the `host` + # identity. This is the only ingress the namespace admits — without it the + # controller pod fails its probes and CrashLoopBackOffs, with nothing in its + # own logs to say why. + - fromEntities: + - host + - remote-node + egress: + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + - port: "53" + protocol: TCP + # EndpointMonitor watch, Ingress reads, and the startup GET of the config + # Secret. + - toEntities: + - kube-apiserver + # api.uptimerobot.com. Not reached at all until an EndpointMonitor exists: + # the controller makes no API call at startup. + - toEntities: + - world + toPorts: + - ports: + - port: "443" + protocol: TCP