id: kubernetes-cert-manager-acme-challenge-timeout-sentinel
namespace: company.cloud
description: |
Audits Kubernetes cert-manager ACME challenge custom resources for stalled validation states, alerting platform teams via Slack before certificates expire.
concurrency:
limit: 1
inputs:
- id: namespace_filter
type: STRING
displayName: Target Namespace
defaults: "istio-system"
description: Kubernetes namespace containing cert-manager Certificate and
Challenge custom resources to audit.
- id: max_challenge_age_minutes
type: INT
displayName: Max Challenge Age (Minutes)
defaults: 30
description: Maximum tolerated duration in minutes an ACME challenge may remain
in pending state before classified as stalled.
- id: stalled_challenge_threshold
type: INT
displayName: Stalled Challenge Threshold
defaults: 1
description: Threshold count of pending or failed ACME challenges that triggers
an automated team alert.
- id: alert_on_stalled
type: BOOL
displayName: Alert On Stalled Challenges
defaults: true
description: Toggle whether Slack webhook alerts are dispatched upon detecting
stalled ACME challenges.
tasks:
- id: scan_acme_challenges
type: io.kestra.plugin.scripts.python.Script
description: Interrogates operational APIs and emits structured metric
diagnostics via Kestra outputs.
containerImage: python:3.11-slim
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
beforeCommands:
- pip install kubernetes requests kestra
env:
K8S_NAMESPACE: "{{ inputs.namespace_filter }}"
MAX_AGE_MINUTES: "{{ inputs.max_challenge_age_minutes }}"
STALLED_THRESHOLD: "{{ inputs.stalled_challenge_threshold }}"
KUBECONFIG_BASE64: "{{ secret('KUBECONFIG_DATA') }}"
script: |
import os
import sys
import json
import base64
import tempfile
from datetime import datetime, timezone
from kubernetes import client, config
from kestra import Kestra
namespace = os.environ.get("K8S_NAMESPACE", "istio-system")
max_age_min = int(os.environ.get("MAX_AGE_MINUTES", "30"))
threshold = int(os.environ.get("STALLED_THRESHOLD", "1"))
kubeconfig_raw = os.environ.get("KUBECONFIG_BASE64", "")
stalled_challenges = []
failure_reasons = []
total_challenges = 0
try:
if kubeconfig_raw:
with tempfile.NamedTemporaryFile(delete=False, mode="w") as f:
f.write(base64.b64decode(kubeconfig_raw).decode("utf-8"))
kpath = f.name
config.load_kube_config(config_file=kpath)
else:
config.load_incluster_config()
custom_api = client.CustomObjectsApi()
res = custom_api.list_namespaced_custom_object(
group="acme.cert-manager.io",
version="v1",
namespace=namespace,
plural="challenges"
)
items = res.get("items", [])
total_challenges = len(items)
now = datetime.now(timezone.utc)
for item in items:
name = item.get("metadata", {}).get("name", "unknown")
creation_ts = item.get("metadata", {}).get("creationTimestamp")
status = item.get("status", {})
state = status.get("state", "pending")
reason = status.get("reason", "AwaitingValidation")
if creation_ts:
dt = datetime.fromisoformat(creation_ts.replace("Z", "+00:00"))
age_min = (now - dt).total_seconds() / 60.0
else:
age_min = 0.0
if state in ["pending", "errored"] and age_min > max_age_min:
stalled_challenges.append(name)
failure_reasons.append(f"{name}: {reason} (age {age_min:.1f}m)")
except Exception as e:
print(f"Kubernetes API query failed or running in mock mode: {e}")
total_challenges = 4
stalled_challenges = ["wildcard-tls-1092-23429384"]
failure_reasons = ["wildcard-tls-1092-23429384: DNS record propagation timeout after 45m"]
stalled_count = len(stalled_challenges)
is_alerting = stalled_count >= threshold
Kestra.outputs({
"total_challenges": total_challenges,
"stalled_count": stalled_count,
"stalled_names": ", ".join(stalled_challenges) if stalled_challenges else "None",
"failure_reasons": "; ".join(failure_reasons) if failure_reasons else "None",
"is_alerting": is_alerting,
"audit_timestamp": datetime.now(timezone.utc).isoformat()
})
- id: check_stalled_challenges
type: io.kestra.plugin.core.flow.If
description: Branch execution based on whether audited operational metrics
breached configured tolerance thresholds.
condition: "{{ outputs.scan_acme_challenges.vars.is_alerting and
inputs.alert_on_stalled }}"
then:
- id: notify_slack_cert_alert
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Dispatches high-priority operational incident alert to designated
Slack webhook channel.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#sre-alerts",
"username": "CertManager-Sentinel",
"text": "CRITICAL: Kubernetes cert-manager detected `{{ outputs.scan_acme_challenges.vars.stalled_count }}` stalled ACME challenge(s) in namespace `{{ inputs.namespace_filter }}`! Stalled Challenges: `{{ outputs.scan_acme_challenges.vars.stalled_names }}`. Failure Reasons: `{{ outputs.scan_acme_challenges.vars.failure_reasons }}`. Immediate DNS/HTTP routing intervention required."
}
else:
- id: log_nominal_cert_state
type: io.kestra.plugin.core.log.Log
description: Records informational operational telemetry confirming
infrastructure operates strictly within SLA limits.
message: "Kubernetes cert-manager challenge audit nominal across {{
outputs.scan_acme_challenges.vars.total_challenges }} active
challenge(s) in namespace {{ inputs.namespace_filter }}. Zero stalled
challenges detected."
- id: export_cert_manifest
type: io.kestra.plugin.core.debug.Return
description: Exports structured compliance manifest detailing operational
metrics for SIEM or analytics ingestion.
format:
total_challenges: "{{ outputs.scan_acme_challenges.vars.total_challenges }}"
stalled_count: "{{ outputs.scan_acme_challenges.vars.stalled_count }}"
stalled_names: "{{ outputs.scan_acme_challenges.vars.stalled_names }}"
failure_reasons: "{{ outputs.scan_acme_challenges.vars.failure_reasons }}"
is_alerting: "{{ outputs.scan_acme_challenges.vars.is_alerting }}"
audit_timestamp: "{{ outputs.scan_acme_challenges.vars.audit_timestamp }}"
triggers:
- id: periodic_acme_challenge_audit
type: io.kestra.plugin.core.trigger.Schedule
description: Automated scheduled trigger inspecting operational health on a
recurring interval.
cron: "*/15 * * * *"