<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Elastic Observability Labs - Articles by Adrian Chen</title>
        <link>https://www.elastic.co/observability-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Fri, 07 Aug 2026 12:17:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Elastic Observability Labs - Articles by Adrian Chen</title>
            <url>https://www.elastic.co/observability-labs/assets/observability-labs-thumbnail.png</url>
            <link>https://www.elastic.co/observability-labs</link>
        </image>
        <copyright>© 2026. Elasticsearch B.V. All Rights Reserved</copyright>
        <item>
            <title><![CDATA[Automated Reliability: The Architecture of Self-Healing Enterprises]]></title>
            <link>https://www.elastic.co/observability-labs/blog/aiops-remediation-elastic-worklfows</link>
            <guid isPermaLink="false">aiops-remediation-elastic-worklfows</guid>
            <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Discover how to close the remediation gap using automation and artificial intelligence. Learn to build self-healing systems that detect, analyse, and fix infrastructure issues automatically. Improve system reliability and eliminate manual operations today.]]></description>
            <content:encoded><![CDATA[<p>Your enterprise systems are built for speed. The organisation has adopted microservices to decouple teams. The strategy is to move to the cloud to scale infrastructure on demand. We’ve started to deploy Kubernetes to orchestrate containers. The team has achieved velocity, but you likely sacrificed clarity. The complexity of your distributed environment now exceeds the cognitive capacity of any single human operator. When systems break, your Mean Time To Resolution (MTTR) suffers not because you lack data, but because the gap between detection and action is too wide.</p>
<p>This guide outlines the strategy to close that gap. It details how you move from passive Observability—watching a dashboard turn red—to proactive auto-remediation, where the system identifies, analyzes, and fixes itself. You will see how <a href="https://www.elastic.co/search-labs/blog/elastic-workflows-automation">Elastic Workflows</a>, Elastic Observability, Out-of-the-box (OOTB) Observability Agents, and Custom Agent in <a href="https://www.elastic.co/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">Agent Builder</a> converge to solve the &quot;Trust Deficit&quot; that plagues traditional automation. We examine the technical implementation of self-healing systems and the business imperatives that make this transition necessary for the modern Chief Information Officer (CIO). You will learn to construct a reliability architecture that acts faster than your best engineer.</p>
<h2>The Entropy of Scale</h2>
<h3>The Failure of Manual Operations</h3>
<p>Systems generate telemetry at machine speed—millions of log lines, metric data points, and traces per second. Yet, your response to failure operates at human speed. You rely on an on-call engineer to wake up, interpret a notification, log in to a VPN, navigate a console, and execute a command. This latency is the &quot;Remediation Gap&quot;. In that gap, revenue bleeds, customer trust erodes, and technical debt accumulates.</p>
<p>Manual remediation fails at scale for three specific reasons: cognitive overload, context switching, and the fear of action.</p>
<h4>Cognitive Overload and the Signal-to-Noise Problem</h4>
<p>Your Observability tools ingest petabytes of data. Finding the signal in that noise is the primary challenge for your Site Reliability Engineers (SREs). Traditional alerting relies on static thresholds where a rule is set at a specific value, for example, &quot;Alert if CPU &gt; 90%.&quot; In a dynamic cloud environment, static thresholds are insufficient as they create a lot of noise from false positives. For example, a database compaction process might spike CPU usage safely every night, or a Java application might naturally consume memory up to its heap limit before garbage collection. This is perfectly normal and expected behaviour in the environment, which creates confusion and fatigue for SREs when they’re alerted to false-positive issues in the environment.</p>
<p>When you bombard engineers with these low-fidelity alerts, you create &quot;Alert Fatigue”, where SREs become overwhelmed with large volumes of alerts, many of which are false positives. When this happens, SREs stop trusting the pager. They treat alerts as suggestions rather than mandates, which impacts responsiveness to fixing real issues. Many organisations look to automation as the solution to alert fatigue, to fast-track response to issues in the environment. However, when alerts are inaccurate and untrustworthy, this creates more issues than it solves. If you automate a restart based on a false positive, you turn your monitoring system into a chaos monkey that attacks your own infrastructure.</p>
<h4>The Cost of Context Switching</h4>
<p>Consider the anatomy of a typical incident. An alert fires in your Observability platform. The engineer sees &quot;High Latency on Service Checkout.&quot; Now the toil begins. They open a new tab for the infrastructure provider to check the pod status. They open another tab for the APM traces. They open Jira to see recent changes. They open Slack to ask if anyone pushed code as part of a change.</p>
<p>This fragmentation of data is expensive. Every context switch breaks the analyst's flow, widening the Remediation Gap through delays in determining the root cause and resolution fix. Context switching interrupts an analyst's focus. In a major incident, you need analysts to be focused on resolving the issue at hand, rather than losing time switching between tools and data, sets trying to work out what has happened. You need the context and the control plane to exist in the same interface so analysts can identify and resolve issues as fast as possible. The separation of &quot;looking&quot; and &quot;doing&quot; forces your engineers to act as human routers, copying IDs and error messages between incompatible tools.</p>
<h4>The Blast Radius and the Fear of Automation</h4>
<p>You hesitate to turn on auto-remediation because you fear the unknown impact of automation operating outside the expected bounds. A script that restarts a service works fine when one node fails. If a global configuration error causes all nodes to fail simultaneously, that same script might trigger a cascading restart loop that takes down the entire platform.</p>
<p>This fear leads to &quot;Change Management Paralysis&quot;. You implement rigid approval boards and manual checks to prevent automation from running amok. You trade speed for reliability, but ultimately lose both. The solution is not to avoid automation, but to implement &quot;Safe Automation&quot; that understands dependency chains and business impact.</p>
<h2>The Architecture of Action</h2>
<h3>Unified Observability and Remediation</h3>
<p>To reduce the Remediation Gap, you must combine your observability data and action systems . You cannot have one tool for seeing and another for acting. Elastic Observability and Elastic Workflows provide this unification, enabling organisations to take intelligent response actions to proactively remediate issues in the environment. You ingest logs, metrics, and traces into Elastic, where intelligence, machine-learning and AI-driven analysis is done out of the box, against your environment’s behaviour baseline. From here, Elastic can use AIOps to detect anomalies with high precision and engage with proactive notifications and automations to quickly resolve the issue. You use the AI to diagnose the root cause and understand the resolution steps. You use Workflows to execute the fix and resolve the issue.</p>
<p>Combining Elastic Observability, AI &amp; Agent Builder, and Workflows creates a closed-loop architecture for automatically resolving issues:</p>
<ol>
<li><strong>Sense:</strong> Ingest telemetry.</li>
<li><strong>Think:</strong> Analyze with Machine Learning and AI.</li>
<li><strong>Act:</strong> Trigger a Workflow.</li>
<li><strong>Verify:</strong> Measure the result with telemetry.</li>
</ol>
<h3>Elastic Workflows: The Orchestration Engine</h3>
<p>Elastic Workflows is the mechanism that turns insight into action. It sits directly within the Elastic platform, which means it has zero-latency access to your data. This is key when taking intelligent response actions to resolve performance issues in a timely manner, as context can be given to the automation for auto-analysis and informed logical execution.</p>
<p>A Workflow acts as a structured &quot;recipe&quot; for automation. It defines a sequence of steps that execute based on a trigger. Workflows are defined in YAML, which allows you to treat operations as code. You version control them, review them in Pull Requests, and test them before deployment.</p>
<h3>AIOps and SLOs: The Vitals of the Enterprise</h3>
<p>You solve the &quot;Trust Deficit&quot; by moving away from static thresholds and adopting <strong>Service Level Objectives (SLOs)</strong> and AIOps.</p>
<h4>Understanding SLOs through the &quot;Vital Signs&quot; Analogy</h4>
<p>Think of your IT system as a patient in a hospital. An SLO is the target for the patient's vital signs (e.g., &quot;Heart rate must stay between 60 and 100&quot;).</p>
<ul>
<li><strong>SLI (Service Level Indicator):</strong> This is the heart monitor. It measures the reality (e.g., &quot;Current heart rate is 105&quot;).</li>
<li><strong>Error Budget:</strong> This is the patient's resilience. The patient can tolerate a heart rate of 105 for a few minutes (budget consumption) without permanent damage. However, if it stays there for an hour (budget exhaustion), you have a crisis.</li>
</ul>
<p>In auto-remediation, we use the <strong>Burn Rate</strong> of this budget to determine urgency. A slow burn (minor degradation) is like a slight fever; you treat it with medication (email notification) in the morning. A fast burn (outage) is cardiac arrest; you treat it with a defibrillator (immediate pager/auto-restart) instantly.</p>
<h4>Elastic Agent Builder: The Cognitive Engine</h4>
<p>Standard automation is brittle because it is deterministic. It follows strict rules: &quot;If X, do Y.&quot; Real-world incidents are messy. They require probabilistic reasoning and adaptation. Elastic Agent Builder provides this capability using Generative AI and Retrieval Augmented Generation (RAG).</p>
<p>Agent Builder allows you to define multiple agents that can be used to build Agentic AI experiences. It does more than just &quot;chat”, by integrating into Elastic Workflows as a reasoning agent that can interact with other agents as needed.</p>
<ul>
<li><strong>Contextual Analysis:</strong> It looks at the specific logs related to an alert. It deciphers cryptic stack traces. It determines whether there are related alerts or cases.</li>
<li><strong>Institutional Memory:</strong> It searches your internal runbooks, Jira tickets, and Confluence pages (ingested into Elasticsearch). It finds that &quot;Error 503 on Service Payment&quot; was fixed last month by rotating a specific key.</li>
<li><strong>Code Generation:</strong> It writes the specific ES|QL query needed to verify the impact of the issue.</li>
</ul>
<h2>Technical Implementation Scenarios</h2>
<p>We will now look at two distinct scenarios: an <strong>Application-Level</strong> issue (SLO breach) and an <strong>Infrastructure-Level</strong> issue (Disk Space Exhaustion).</p>
<h3>Scenario A: Application SLO Breach (Smart Escalation)</h3>
<p><strong>The Objective:</strong> Handle a service degradation intelligently. If the service is merely &quot;degraded&quot; (breaching a warning threshold but not down) and the time is near the start of the business day, we route the alert to email to avoid waking up engineers. If the service is &quot;down&quot; or if it is a critical off-hours failure, we page the on-call team.</p>
<p><strong>Workflow Logic:</strong></p>
<ol>
<li><strong>Trigger:</strong> SLO Burn Rate alert fires.</li>
<li><strong>Check Context:</strong> Calculate if it is &quot;Start of Business Day.&quot;</li>
<li><strong>Decision:</strong> If Alert is Warning AND Time is Start of Day → Email. Else → PagerDuty.</li>
</ol>
<p>Workflow YAML Definition:</p>
<pre><code>name: Smart Escalation
enabled: false
description: Escalation based on business hours
triggers:
  - type: alert

steps:
  # Check for start day
  - name: start_day_check
    type: console
    with:
      message: |
        {%- assign hour = &quot;now&quot; | date: &quot;%H&quot;, &quot;Australia/Sydney&quot; | plus: 0 %}
        {%- if hour &gt;= 7 and hour &lt;= 9 %}
          true
        {%- else %}
          false
        {%- endif %}
  - name: get_severity
    type: console
    with:
      message: '{{event.alerts[0][&quot;kibana.alert.severity&quot;] | default: &quot;low&quot;}}'
  # routing logic
  - name: routing_logic
    type: if
    condition: 'steps.get_severity.output: critical AND steps.trimmed_start_day.output: false'
    steps:
      - name: hard_notification
        type: pagerduty
        connector-id: &quot;# Enter connector UUID here&quot;
        with:
          eventAction: &quot;trigger&quot;
          severity: &quot;{{steps.get_severity.output}}&quot;
          summary: 'CRITICAL: {{event.alerts[0][&quot;monitor.name&quot;]}} SLO breached'
        
    else:
      - name: soft_notification
        type: email
        connector-id: Elastic-Cloud-SMTP
        with:
          to: [&quot;&lt;your_email&gt;&quot;]
          subject: '{{event.alerts[0][&quot;kibana.alert.rule.name&quot;]}} Failed'
          message: 'Reason: {{event.alerts[0][&quot;kibana.alert.reason&quot;]}}'
</code></pre>
<h3>Scenario B: Infrastructure Auto-Remediation (Disk Space)</h3>
<p><strong>The Challenge:</strong></p>
<p>In a large enterprise, &quot;Out of Disk Space&quot; is a silent killer. When /var/log fills up:</p>
<ul>
<li><strong>Nginx</strong> crashes because it cannot write access logs.</li>
<li><strong>MySQL</strong> panics because it cannot write transaction logs.</li>
<li><strong>SSH</strong> fails because it cannot write to /var/log/auth.log, locking you out of the server you need to fix.</li>
</ul>
<p>By the time the server stops responding to ping, it is too late. You need to catch the <em>trend</em> while the server is still reporting to your observability solution.</p>
<p><em>Note: To allow grouping of the infra service, we utilized the <a href="https://www.elastic.co/docs/reference/fleet/agent-policy#add-custom-fields">Add custom field</a> feature of the Elastic Agent policy to add the my_org.custom.service field.</em></p>
<p><strong>The Workflow:</strong></p>
<ol>
<li><strong>Phase 1: Context &amp; Health Verification</strong>
<ul>
<li><strong>Detect:</strong> Elastic detects disk usage &gt; 90% over time</li>
<li><strong>Verify (Freshness):</strong> Confirm the host is still sending logs (it hasn't crashed yet).</li>
<li><strong>Impact Analysis (HA):</strong> Check if other instances of this service are healthy. If 9/10 nodes are healthy, this is a P3 issue. If 1/2 nodes are healthy, it is P1.</li>
</ul>
</li>
<li><strong>Phase 2: Remediate</strong>
<ul>
<li><strong>Remediate:</strong> Trigger an <strong>Ansible Tower</strong> playbook to rotate logs and clear cache.</li>
<li><strong>Audit:</strong> Log the result.</li>
</ul>
</li>
</ol>
<p><strong>Workflow YAML Definition:</strong></p>
<pre><code>name: &quot;Disk_Space_Remediation&quot;
description: &quot;Detects full disk, checks HA status, and triggers Ansible Tower cleanup.&quot;
enabled: true
consts:
  ansible_webhook: &quot;&lt;your_ansible_url&gt;&quot;
triggers:
  - type: alert
steps:
  # ---------------------------------------------------------
  # Phase 1: Context &amp; Health Verification
  # ---------------------------------------------------------
  # Check 1: Are logs still flowing? (Avoid false positives from dead nodes)
  - name: check_log_freshness
    type: elasticsearch.esql.query
    with:
      query: |
        FROM logs-*
        | WHERE host.name == &quot;{{event.alerts[0]['host.hostname']}}&quot;
        | STATS latest_log = MAX(@timestamp)
        | EVAL latency_sec = (TO_LONG(NOW()) - TO_LONG(latest_log)) / 1000
        | LIMIT 1
  # Get how many hosts serving that service within 24h
  - name: count_service_hosts
    type: elasticsearch.esql.query
    with:
      query: |
        FROM logs-*
        | WHERE @timestamp &gt;= (NOW() - TO_TIMEDURATION(&quot;24 hours&quot;)) AND my_org.custom.service == &quot;{{event.alerts[0]['kibana.alert.grouping'].my_org.custom.service}}&quot;
        | STATS COUNT_DISTINCT(host.hostname)
  # Get how many hosts of the service have alerts within the last 15m 
  - name: count_distinct_alerts
    type: elasticsearch.esql.query
    with:
      query: |
        FROM .alerts-observability.metrics.alerts-default
        | WHERE @timestamp &gt;= (NOW() - TO_TIMEDURATION(&quot;15 minutes&quot;)) AND kibana.alert.grouping.my_org.custom.service == &quot;{{event.alerts[0]['kibana.alert.grouping'].my_org.custom.service}}&quot;
        | STATS COUNT_DISTINCT(kibana.alert.grouping.host.name)
  # ---------------------------------------------------------
  # Phase 2: Remediation
  # ---------------------------------------------------------
  - name: routing_logic
    type: if
    condition: 'steps.check_log_freshness.output.values[0][1] &lt;= 300'
    steps:
      # Action: Trigger Ansible Tower Job Template (Log Cleanup)
      # We pass the hostname as an 'extra_var' to the playbook
      - name: trigger_ansible
        type: http
        with:
          url: &quot;{{consts.ansible_webhook}}&quot;
          method: POST
          body:
            extra_vars:
              target_host: &quot;{{event.alerts[0]['host.hostname']}}&quot;
              remediation_type&quot;: clear_var_log
          headers: 
            Accept: application/json
            Content-Type: application/json
      # Action: Notify SRE with Context (SLO/HA)
      # If HA is healthy (&gt;75% of the nodes), send standard notification. If HA is at risk, escalate.
      - name: notify_result
        type: console
        with:
          message: |
            {%- assign num_alerted_hosts = steps.count_distinct_alerts.output.values[0][0] | plus: 0 %}
            {%- assign num_service_hosts = steps.count_service_hosts.output.values[0][0] | plus: 0 %}
            {%- assign critical_ratio = num_alerted_hosts | divided_by: num_service_hosts | times: 1.0 %}
            {%- if critical_ratio &gt;= 0.25 %} #ops-critical {%- else %} #ops-alerts{%- endif %}
            *Auto-Remediation Triggered: Disk Space*
            *Host:* {{event.alerts[0]['host.hostname']}}
            *Service:* {{event.alerts[0]['kibana.alert.grouping'].my_org.custom.service}}
            *Number of alerted instances:* {{steps.count_distinct_alerts.output.values[0][0]}}/{{steps.count_service_hosts.output.values[0][0]}}.
            *Action:* Ansible Playbook ID 15 triggered.
    else:
      # Fallback: If logs are stale, the agent might be dead. Manual intervention needed.
      - name: manual_escalation
        type: console
        with:
          message: |
            CRITICAL: Host {{event.alerts[0]['host.hostname']}} Unresponsive &amp; Disk Full

</code></pre>
<p><em>Note: The notify_result is using a console step placeholder to consolidate demo output.</em></p>
<h4>Analysis of the Infrastructure Scenario</h4>
<p>This workflow demonstrates a mature &quot;self-healing&quot; capability:</p>
<ol>
<li><strong>Predictive vs. Reactive:</strong> By triggering at 90% (a safe threshold before systems start to crash), we use the Elastic Agent to fix the issue before the OS locks up the filesystem.</li>
<li><strong>Safety Checks:</strong> The check_log_freshness step ensures we don't try to run a remote playbook on a server that has already disconnected, which would likely fail and create noise.</li>
<li><strong>Business Awareness (HA):</strong> The notification logic (check_ha_status) understands priority. A disk issue on 1 of 50 web servers is a standard alert. A disk issue on 1 of 2 database nodes is a critical emergency. The workflow adjusts the Slack channel destination dynamically based on this reality.</li>
</ol>
<h3>Scenario C: AI-Augmented Triage (The &quot;Digital Specialist&quot;)</h3>
<p><strong>The Challenge:</strong></p>
<p>Application errors are often ambiguous. An alert says &quot;Payment Failed: Error 9001.&quot; No dashboard explains this. The answer lies in a PDF runbook titled <em>Legacy Payment API v2.0</em> stored on a forgotten SharePoint or Wiki, or in experienced SRE or application team members' heads.</p>
<p>Usually, the SRE wastes 30 minutes searching for this document or finding a team member with the contextual background knowledge.</p>
<p><strong>The Solution:</strong></p>
<p>By combining Search and Generative AI, we can couple telemetry and context into a single system. Using a RAG architectural approach, Elastic can surface relevant contextual information alongside telemetry using natural language queries to support analysts without switching tools. We index our runbooks and documentation into Elastic using Search tools and techniques which allow contextual information to be stored for use by AI Agents. This data provides the AI Agents with important contextual information about the environment, which can be used as part of intelligent automation workflows that can be applied to solve performance issues. In this scenario, when the workflow triggers, it searches an index (‘sre-knowledge-base’) to fetch any applicable runbooks which would be used to solve the issue, or documented knowledge articles. This information is then handed to the AI Agent to synthesize a fix and provide the analyst with recommended action steps.</p>
<p><strong>The Workflow:</strong></p>
<ol>
<li><strong>Phase 1: Retrieve Knowledge</strong>
<ul>
<li><strong>Trigger:</strong> &quot;Application Error&quot; alert fires.</li>
<li><strong>Retrieve Context (RAG):</strong> The workflow performs a <a href="https://www.elastic.co/docs/solutions/search/get-started/semantic-search"><strong>Semantic Search</strong></a> against the ‘sre-knowledge-base’ index using the error message from the alert to find relevant contextual information about the error.</li>
</ul>
</li>
<li><strong>Phase 2: AI Analysis</strong>
<ul>
<li><strong>Synthesize (AI):</strong> The workflow passes the <em>Alert Logs</em> and the <em>Retrieved Runbook Snippets</em> to the AI Agent, along with a prompt which  asks: &quot;Based on these logs and this runbook, what is the fix?&quot;</li>
</ul>
</li>
<li><strong>Phase 3: Communication</strong>
<ul>
<li><strong>Notify:</strong> The workflow posts the analysis to Slack.</li>
</ul>
</li>
</ol>
<p><strong>Workflow YAML Definition:</strong></p>
<pre><code>name: AI_Runbook_Assistant
enabled: true
description: Uses RAG to find runbooks for unknown errors and suggests fixes.
triggers:
  - type: alert

steps:
  # ---------------------------------------------------------
  # Phase 1: Retrieve Knowledge (The &quot;Memory&quot; Lookup)
  # ---------------------------------------------------------
  
  # Perform a Semantic Search against internal runbooks
  # We use the alert reason as the search query

  - name: search_knowledge_base
    type: elasticsearch.esql.query
    with:
      query: |
        FROM sre-knowledge-base*
        | WHERE semantic_text : &quot;What caused '{{event.alerts[0]['kibana.alert.rule.parameters'].criteria[0].value}}' of '{{event.alerts[0]['kibana.alert.grouping'].service.name}}' and if available, how to resolve?&quot;
        | KEEP title, text
        | LIMIT 3

  # ---------------------------------------------------------
  # Phase 2: AI Analysis (The &quot;Specialist&quot; Reasoning)
  # ---------------------------------------------------------
  # Pass the retrieved knowledge + live logs to the AI
 - name: ai_analysis
    type: ai.agent
    with:
      agent_id: observability.agent
      message: |
        ISSUE DETECTED:
        {{ event.alerts[0][&quot;kibana.alert.context&quot;].conditions}} for {{event.alerts[0]['kibana.alert.grouping'].service.name}} 

        RELEVANT INTERNAL RUNBOOKS FOUND:
        {% for doc in steps.search_knowledge_base.output.values limit: 3 offset: 0 %}
        - Title: {{doc[0]}}
          Content: {{doc[1]}}
        {% endfor %}

        TASK:
        Analyze the issue. If the runbooks provide a solution, summarize it in a concise manner. 
        If not, suggest standard triage steps.
        Provide the output in Slack Markdown format.

  # ---------------------------------------------------------
  # Phase 3: Communication
  # ---------------------------------------------------------
  
  - name: notify_slack
    type: console 
    with:
      message: |
        channel: &quot;#incident-war-room&quot;
        text: |
          🚨 *Incident Detected*
          *Issue:* {{ event.alerts[0][&quot;kibana.alert.context&quot;].conditions}} for {{event.alerts[0]['kibana.alert.grouping'].service.name}} 
        
          🤖 *AI Agent Analysis:*
          {{ steps.ai_analysis.output.content }}
          
</code></pre>
<p><em>Note:</em> <em>The notification to Slack is simulated using a console step to consolidate demo output.</em></p>
<h4>Analysis of the AI-Augmented Scenario</h4>
<p>This workflow changes the nature of incident response:</p>
<ol>
<li><strong>Instant Context:</strong> It automates the &quot;Search&quot; phase of troubleshooting. The engineer enters the war room and the relevant runbook is <em>already there</em>.</li>
<li><strong>Semantic Understanding:</strong> Unlike traditional keyword search, the RETRIEVE command uses semantic search techniques to find the required runbook, even if the wording doesn't match exactly (e.g., searching for &quot;Login Fail&quot; finds &quot;Authentication Timeout&quot;).</li>
<li><strong>Automated AI Triage</strong>: Leverage AI agents for first pass analysis and suggested remediation steps, giving analysts a head start on identifying the root cause and remediation steps. Get the output automatically sent to the engineers for proactive response action.</li>
<li><strong>Reduced MTTR:</strong> By putting the solution in front of the engineer immediately, you eliminate the initial investigation delay.</li>
</ol>
<p>The screenshots below show the transparency Workflows provides across the data analysed and transferred in each step of the process. You can see how Search is used to retrieve the appropriate run book, which is then incorporated with automated AI analysis for a comprehensive notification to engineers, which contains initial triage and remediation steps.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image1.png" alt="Elastic Workflows - Search Internal Knowledge Base" /></p>
<p><em>Figure 1: Output from automated Search for runbooks using Workflow</em></p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image2.png" alt="Elastic Workflows - AI Analysis" /></p>
<p><em>Figure 2: Output from automated AI triage of error using Elastic Workflows</em></p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image3.png" alt="Elastic Workflows - Notify Slack" /></p>
<p><em>Figure 3: AI Analysis, which is sent to Slack using Elastic Workflows</em></p>
<h2>The Agentic Shift: Integrating Elastic Agent Builder</h2>
<p>While workflows are powerful, they are deterministic. They follow the &quot;If X, then Y&quot; logic you explicitly code. However, modern operations often require probabilistic reasoning—the ability to figure out &quot;X&quot; in the first place. Additionally, RAG-based architectures often rely on a one-shot approach to prompt response, which can be limited for complex scenarios. This is where the <strong>Elastic Agent Builder</strong> can be used as part of Agentic AI experiences, where AI Agents are able to reason and interact autonomously in a series of steps to provide improved and highly accurate responses.</p>
<h3>Bridging Reasoning and Action</h3>
<p><strong>Elastic Agent Builder</strong> allows you to create specialized AI agents that live within your Elastic environment. These agents have secure access to data in your Elastic deployment - both telemetry and unstructured information in the knowledge base for contextual background. More crucially, they have the ability to dynamically query this information and access  <strong>Workflows as Tools,</strong> allowing them to assess and reason, and then take action appropriately .</p>
<p>This creates a powerful bidirectional architecture:</p>
<ul>
<li><strong>Elastic Workflows call Agents:</strong> A workflow pauses to ask an agent for further investigation and decision (e.g., &quot;Is this log pattern malicious?&quot;).</li>
<li><strong>Agents call Elastic Workflows:</strong> An agent, during a chat with an SRE, calls a workflow to execute a safe, pre-approved action (e.g., &quot;Run the Disk Cleanup Workflow&quot;).</li>
</ul>
<h3>The &quot;Hands&quot; of the AI (MCP)</h3>
<p>A major risk with AI in operations is hallucination. You do not want an LLM to guess commands and steps to execute. When using AI in automation, we need to give it tools and information to ground the LLM and ensure responses and actions are accurate and controlled. You solve this with the <strong>Model Context Protocol (MCP)</strong>.</p>
<p>MCP is the standard that allows your Agent to connect to external systems safely. Instead of giving the Agent raw shell access, you give it a &quot;Tool&quot; called cleanup_disk, which has a specific command attached to it in the external system. Different types of tools can be defined, including Workflows, ES|QL queries or index patterns that expose a set of indices to the Agent. For example, instead of searching the ‘sre-knowledge-base’ index as a step in the workflow, a tool could be created to allow Agents to query the data autonomously when asked to do analysis. Elastic has the ability to serve tools via MCP to other systems as needed, as well as to native Agents defined in Agent Builder. For example, a ‘cleanup_disk’ tool maps directly to the deterministic <strong>workflow</strong> we built in Scenario B, which can be used as part of a revised AI-powered remediation flow:</p>
<p><strong>The Human-in-the-loop Interaction Flow:</strong></p>
<ol>
<li><strong>SRE:</strong> &quot;Agent, the checkout service is failing.&quot;</li>
<li><strong>Agent:</strong> Queries logs (using ES|QL tool). &quot;It looks like the disk is full on node-01.&quot;</li>
<li><strong>SRE:</strong> &quot;Fix it.&quot;</li>
<li><strong>Agent:</strong> &quot;I will run the Disk_Space_Remediation workflow for node-01.&quot; (The agent invokes the workflow via MCP, where it is defined as the ‘cleanup_disk’ tool).</li>
<li><strong>Elastic Workflows:</strong> Executes the Ansible job. (Deterministic, logged, and safe).</li>
</ol>
<p><strong>The Self-Healing Flow:</strong></p>
<ol>
<li><strong>Alert:</strong> &quot;The checkout service is failing.&quot;</li>
<li><strong>Alert Action:</strong> &quot;Run the SRE Agent Runbook workflow&quot;</li>
<li><strong>Elastic Workflows:</strong> Pass the alert and its context to the SRE agent.</li>
<li><strong>SRE Agent:</strong> Queries logs (using ES|QL tool). &quot;It looks like the disk is full on node-01.&quot;</li>
<li><strong>SRE Agent:</strong> I have authorization and the tool to remediate this.</li>
<li><strong>SRE Agent:</strong> &quot;I will run the Disk_Space_Remediation workflow for node-01.&quot; (The agent invokes the workflow via MCP, where it is defined as the ‘cleanup_disk’ tool).</li>
<li><strong>Elastic Workflows:</strong> Executes the Ansible job. (Deterministic, logged, and safe).</li>
</ol>
<h3>Implementation: Connecting Agent Builder</h3>
<p>To enable this, you register your workflows as tools in the Agent Builder UI.</p>
<ol>
<li><strong>Define the Tool:</strong> In Agent Builder, create a new tool. Select &quot;Elastic Workflow”, and provide the tool name and description. The description should be informative, as it is used by the AI Agents in understanding how the tool is used.</li>
<li><strong>Bind the Workflow:</strong> Choose the Disk_Space_Remediation workflow from the drop-down.</li>
<li><strong>Define Schema:</strong> Tell the agent what inputs the workflow needs (e.g., target_host).</li>
<li><strong>Deploy:</strong> Create the AI Agent in the Agent Builder UI, which has access to the tool we just created. It can now execute the defined workflow, ensuring a sequence of actions is completed in a deterministic manner.</li>
</ol>
<p><strong>Custom Knowledge Base Tool creation API:</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  &quot;id&quot;: &quot;search_knowledge_base&quot;,
  &quot;type&quot;: &quot;esql&quot;,
  &quot;description&quot;: &quot;Semantic Search against internal runbooks&quot;,
  &quot;tags&quot;: [
    &quot;knowledge-base&quot;,
    &quot;observability&quot;
  ],
  &quot;configuration&quot;: {
    &quot;query&quot;: &quot;&quot;&quot;FROM sre-knowledge-base*
        | WHERE semantic_text : &quot;What caused '?alert_criteria' of '?service_name' and if available, how to resolve?&quot;
        | KEEP title, text
        | LIMIT 3&quot;&quot;&quot;,
    &quot;params&quot;: {}
  }
}
</code></pre>
<p><strong>Custom Cleanup Tool creation API:</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  &quot;id&quot;: &quot;cleanup_disk&quot;,
  &quot;type&quot;: &quot;workflow&quot;,
  &quot;description&quot;: &quot;Predefined Disk Space remediation workflow to perform the required cleanups. &quot;,
  &quot;tags&quot;: [],
  &quot;configuration&quot;: {
    &quot;workflow_id&quot;: &quot;workflow-0f1f5ba7-0e75-4469-92b1-b85a032074e5&quot;,
    &quot;wait_for_completion&quot;: true
  }
}
</code></pre>
<p><strong>Custom SRE Agent creation API:</strong></p>
<pre><code>POST kbn:/api/agent_builder/agents
{
  &quot;id&quot;: &quot;sre_agent&quot;,
  &quot;name&quot;: &quot;SRE Agent&quot;,
  &quot;description&quot;: &quot;An SRE agent to analyze issues, provide summarized solutions from runbook where they exist or suggest standard triage steps when they don't exist. &quot;,
  &quot;labels&quot;: [],
  &quot;avatar_color&quot;: &quot;#61A2FF&quot;,
  &quot;avatar_symbol&quot;: &quot;🕵&quot;,
  &quot;configuration&quot;: {
    &quot;instructions&quot;: &quot;&quot;&quot;You are a Senior SRE.

The issues/alerts detected will be provided in the following format
ISSUE DETECTED:
&lt;alert_criteria&gt; for &lt;service_name&gt;

Use the search_knowledge_base tool to extract known runbooks based on the alert_criteria and service name. 
For each runbook result, format it as follows.
- Title: &lt;title from search_knowledge_base&gt;
   Content: &lt;text from search_knowledge_base&gt;

TASK:
Analyze the issue. If the runbooks provide a solution, summarize it in a concise manner. 
If not, suggest standard triage steps. If the determined solution is to remediate via a disk cleanup, use the cleanup_disk tool. 
Provide the output in Slack Markdown format. Avoid unnecessary token usage be concise whilst descriptive. &quot;&quot;&quot;,
    &quot;tools&quot;: [
      {
        &quot;tool_ids&quot;: [
          &quot;platform.core.get_workflow_execution_status&quot;,
          &quot;observability.get_alerts&quot;,
          &quot;search_knowledge_base&quot;,
          &quot;cleanup_disk&quot;
        ]
      }
    ]
  }
}
</code></pre>
<p><strong>Workflow YAML Definition:</strong></p>
<pre><code>version: &quot;1&quot;
name: SRE Agent Runbook
description: Use a custom SRE agent to suggests fixes and perform disk cleanup where appropriate.
enabled: true
triggers:
  - type: alert
steps:
  - name: sre_analysis
    type: ai.agent
    with:
      agent_id: sre_agent
      message: |
        ISSUE DETECTED:
        {{ event.alerts[0][&quot;kibana.alert.context&quot;].conditions}} for {{event.alerts[0]['kibana.alert.grouping'].service.name}} 
  - name: notify_slack
    type: console
    with:
      message: |+
        channel: &quot;#incident-war-room&quot;
        text: |
          🚨 *Incident Detected*
          *Issue:* {{ event.alerts[0][&quot;kibana.alert.context&quot;].conditions}} for {{event.alerts[0]['kibana.alert.grouping'].service.name}} 

          🤖 *SRE Agent Analysis:*
          {{ steps.sre_analysis.output }}
</code></pre>
<p><em>Note:</em> <em>The notification to Slack is simulated using a console step to consolidate demo output.</em></p>
<p><strong>Workflow YAML Definition:</strong></p>
<p>The screenshots below show how we create custom tools for the agent, then create a custom agent scoped to specific tooling. The level of control simplifies the workflow and sets up the framework for how users and agents interact and their boundaries.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image4.png" alt="Custom Tool Creation - search_knowledge_base" /></p>
<p><em>Figure 4: Create a custom search_knowledge_base tool</em></p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image5.png" alt="Custom Tool Creation - cleanup_disk" /></p>
<p><em>Figure 5: Create a custom cleanup_disk tool</em></p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image6.png" alt="Custom Agent Creation - SRE Agent" /></p>
<p><em>Figure 6: Create a custom SRE Agent with defined tools and Instructions</em></p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/image6.png" alt="Elastic Workflows with a custom SRE Agent" /></p>
<p><em>Figure 7: Leverage the custom SRE Agent within Elastic Workflows</em></p>
<p>This shifts your operating model from reactive, manual human-driven analysis and action to a proactive, automated analysis and action model. Using the tools explored in this article, Elastic brings together data, context and remediation for an intelligent, automated response that is reliable and controlled, with the option for ‘Human-in-the-loop’ check points or full AI-driven automation. This supports analysts in bridging the remediation gap and reducing MTTR by using AI and automation to augment root cause analysis efforts.</p>
<h2>Conclusion</h2>
<p>You operate in an era when the complexity of your systems has outpaced the capacity for manual management. The &quot;Remediation Gap&quot; is the single largest source of inefficiency in your IT operations. You cannot hire enough engineers to close it. You must close it with automation.</p>
<p>Proactive operations rely on bringing together telemetry data, contextual information and automation and AI capabilities to reduce time to repair. Elastic Observability provides the vision across systems in your environment. Elastic Workflows provides the hands to automate response actions. Elastic Agent Builder provides the brain for intelligent automation and AI-assisted triage. By unifying these three elements, you build a system that is not just monitored, but resilient. You move from a reactive posture—waiting for the phone to ring—to a proactive one, where the system heals itself before the customer notices.</p>
<p><em>For more examples of Elastic Workflows checkout the <a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started">documentation</a> and this <a href="https://github.com/elastic/workflows">GitHub repository</a>.</em></p>
<p>Start small. Pick one recurring issue. Build one workflow. Measure the time saved. Trust is built on evidence. Once you prove that the machine can fix the machine, you change the nature of your operations forever.</p>
<p>Sign up for <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a> or <a href="https://cloud.elastic.co/registration">Elastic Cloud</a> and try this out.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/aiops-remediation-elastic-worklfows/aiops-remediation-elastic-worklfows.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[TLS certificate monitoring with Elastic Workflows, Synthetics, and Osquery: Eliminate manual renewals]]></title>
            <link>https://www.elastic.co/observability-labs/blog/certificate-monitoring-automation-elastic-workflows</link>
            <guid isPermaLink="false">certificate-monitoring-automation-elastic-workflows</guid>
            <pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Automate TLS certificate monitoring with Elastic Workflows, Synthetics, and Osquery. Detect expiring certificates, rotate, and verify without human intervention.]]></description>
            <content:encoded><![CDATA[<p>Certificate expiry is the only class of production outage that prints its own end date at issuance. There is no anomaly to detect, no model to train, no unknown. And yet, expired certificates still take down payment gateways, internal APIs, and observability stacks every quarter, because the gap between knowing a certificate will expire and acting on it runs through spreadsheets, calendar reminders, and whoever happens to notice first. This post closes that gap: Elastic Synthetics, Osquery, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL</a>, and <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> (generally available in 9.4), wired into a closed-loop TLS certificate monitoring pipeline that detects an expiring certificate, finds every affected host, rotates it, and verifies it. No human intervention required.</p>
<p>The reasoning behind the design (the <em>trust deficit</em>, the <em>remediation gap</em>, and the <strong>sense → think → act → verify</strong> loop that frames everything below) is laid out in <a href="https://www.elastic.co/observability-labs/blog/aiops-remediation-elastic-worklfows">Automated reliability: The architecture of self-healing enterprises</a>. Read that one for the <em>why</em>. Certificates are the right first workload for this pattern because the signal is unambiguous, the corrective action is well-understood, and the blast radius of the <em>automation itself</em> is small enough to build trust before applying the same pattern to noisier domains.</p>
<p>The full source (synthetics journeys, sample workflow YAMLs, sample ES|QL alert queries, the ingest pipeline, and a short-lived-cert local lab) lives in the <a href="https://github.com/adrianchen-es/aiops-synthetics-lab">aiops-synthetics-lab repository</a>. The article walks through the patterns; the repo has the complete artifacts.</p>
<h3>Mapping the build to the closed loop</h3>
<p>Each phase of the closed loop maps to a specific Elastic component and a corresponding artifact in the companion repository. The table below is a quick reference; the diagram that follows shows how data flows between the phases.</p>
<table>
<thead>
<tr>
<th>Phase</th>
<th>Component</th>
<th>Lives in repo at</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Sense</strong></td>
<td>Synthetics browser journeys + Osquery <code>certificates</code> pack</td>
<td><code>journeys/</code>, <code>helpers/tls.ts</code>, <code>docs/ingest-pipeline-synthetics-browser.json</code></td>
</tr>
<tr>
<td><strong>Think</strong></td>
<td>ES|QL alert rule on <code>synthetics-*</code> deduplicated by CN + tags</td>
<td><code>docs/elastic-samples/alerts/</code></td>
</tr>
<tr>
<td><strong>Act</strong></td>
<td>Smart Certificate Rotation Escalation and Remediation workflow</td>
<td><code>docs/elastic-samples/workflows/01-...yaml</code></td>
</tr>
<tr>
<td><strong>Verify</strong></td>
<td>Synthetics re-reads the rotated chain; canary <code>curl_certificate</code> confirms the fleet</td>
<td><code>docs/elastic-samples/workflows/02-...yaml</code>, <code>local-lab/</code></td>
</tr>
</tbody>
</table>
<p><img src="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/cert-closed-loop.svg" alt="Sense → Think → Act → Verify — closed-loop certificate self-healing" /></p>
<p><em>Figure 1: Each phase maps to a specific Elastic primitive, and each phase only relies on clean data from the previous one. No out-of-band coordination, no shared mental state.</em></p>
<hr />
<h2>Discovery: picking the right two methods</h2>
<p>There are four ways to discover certificates, but two methods provide rapid time to value as the following table shows.</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Mechanism</th>
<th>Verdict</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Known endpoints</strong></td>
<td>Playwright browser tests validate the served chain</td>
<td>✅ Validates exactly what the client sees, including intermediates</td>
</tr>
<tr>
<td><strong>OS interrogation</strong></td>
<td>Osquery queries local host keystores</td>
<td>✅ Finds certificates on backend services no synthetic ever calls</td>
</tr>
<tr>
<td><strong>CA log polling</strong></td>
<td>Monitor CA issuance logs</td>
<td>✗ Noisy — certificates are frequently issued and never deployed</td>
</tr>
<tr>
<td><strong>Network discovery</strong></td>
<td>Subnet scanners extract TLS handshakes</td>
<td>✗ Triggers IDS, breaks legacy OT, friction with security ops</td>
</tr>
</tbody>
</table>
<p>Endpoint synthetics gives you the <em>client view</em>. Osquery gives you the <em>host view</em>. A load balancer can serve a valid public cert while a downstream internal host quietly serves an expired one to its peers; invisible from the outside, fully visible from the keystore. You need both.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/cert-two-layer-collection.svg" alt="Two-layer collection model: Synthetics observes from the outside while Osquery reports from the inside" /></p>
<p>The left side (yellow) shows agent-based monitoring: Elastic Agent runs on each host and uses the Osquery <code>certificates</code> pack to read the local operating system keystore — <code>/etc/ssl</code> on Linux, <code>CurrentUser</code> and <code>LocalMachine</code> stores on Windows. This is the <strong>host view</strong>: certificates that services hold regardless of whether those services expose a public endpoint. The right side (blue) shows agentless monitoring: Elastic Synthetics executes browser journeys against reachable HTTPS endpoints and captures the full certificate chain as a client would see it. Both paths normalize output to ECS <code>tls.server.x509.*</code> fields, so the workflow can join them on <code>common_name</code> without any translation step.</p>
<p><em>Figure 2: Synthetics validates what clients see; Osquery enumerates what hosts hold. Both feed ECS-mapped <code>tls.server.x509.*</code> so the workflow can join them on <code>common_name</code> without translation.</em></p>
<hr />
<h2>Architecture overview</h2>
<p>The architecture has four horizontal layers, each corresponding to one phase of the loop. <strong>Signal collection</strong> (top) shows the three data sources: Synthetics is the primary alert driver, Osquery is queried by the workflow for host discovery, and canary <code>curl_certificate</code> validates impact on demand. <strong>Alerting</strong> runs an ES|QL rule on <code>synthetics-*</code> and fires when a cert falls within the expiry window. <strong>Smart escalation and remediation</strong> is the workflow itself: pre-flight severity mapping, host discovery via Osquery, then branching into auto-rotation for certs expiring within 7 days, Jira ticketing for 7-90 day windows, or PagerDuty for anything that needs a human. <strong>Impact validation</strong> runs the canary workflow on demand to measure blast radius before and after any rotation.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/cert-architecture-overview.svg" alt="Certificate self-healing architecture: Synthetics drives the alert, Osquery is queried inside the workflow for host discovery, canary validation is an on-demand impact layer" /></p>
<p><em>Figure 3: Four layers, one closed loop. Synthetics fires the alert, Osquery maps the target hosts, and canary validation measures impact.</em></p>
<hr />
<h2>Prerequisites</h2>
<table>
<thead>
<tr>
<th>Requirement</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Elastic Stack <strong>9.3+</strong></td>
<td>ES|QL <code>LOOKUP JOIN</code> is GA in 9.1; <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> is GA in 9.4 (technical preview in 9.3)</td>
</tr>
<tr>
<td>Elastic Agent via Fleet</td>
<td>Hosts with the <a href="https://docs.elastic.co/integrations/osquery_manager"><strong>Osquery Manager</strong></a> integration enabled</td>
</tr>
<tr>
<td>Elastic Synthetics</td>
<td>A Synthetics project and a private (or managed) location</td>
</tr>
<tr>
<td>Network egress</td>
<td>Cluster reachable to Ansible Tower, Jira, Slack, PagerDuty connectors</td>
</tr>
<tr>
<td>Node.js 18 LTS+</td>
<td>For running the synthetics project locally</td>
</tr>
</tbody>
</table>
<hr />
<h2>Sense: signal collection</h2>
<p>Clone the companion repository to get the complete set of artifacts used throughout this post: Synthetics browser journeys, Osquery pack configuration, Elastic Workflow YAML definitions, ES|QL alert queries, the ingest pipeline definition, and a local lab for testing against short-lived certificates. The <code>npm ci</code> step installs the Node.js dependencies required for running browser journeys locally.</p>
<pre><code class="language-bash">git clone https://github.com/adrianchen-es/aiops-synthetics-lab.git
cd aiops-synthetics-lab
npm ci
</code></pre>
<h3>CSV-driven endpoints</h3>
<p>Endpoints are managed through a CSV file, so adding new hosts requires no code changes. Just edit <code>journeys/tls-browser/tls-target-hosts.csv</code>:</p>
<pre><code class="language-csv">host,criticality,assertionText,assertionSelector
elastic.co,critical,Elastic,h1
internal-api.mycompany.com,high,,
payment-gateway.internal,critical,,
</code></pre>
<p><code>criticality</code> becomes a journey tag (<code>criticality:critical</code>) which flows into the alert payload and drives escalation routing — no CMDB lookup required for basic triage. <code>npm run generate:tls-targets</code> rebuilds the TypeScript host module from the CSV and runs automatically before <code>npm test</code> and <code>npm run push</code>. The repo has additional groups under <code>journeys/tls/</code>, <code>journeys/demos/</code>, and <code>journeys/kibana/</code> for non-browser TLS, badssl-style demos, and a multi-step Kibana login check, respectively <code>npm run push:tls</code> and friends scope deploys to one folder.</p>
<h3>The browser TLS journey</h3>
<p>Each host runs its own journey. Certificate inspection relies on Node's built-in <code>tls</code> module, where <code>cert.fingerprint256</code> is precomputed by OpenSSL during the handshake, so it incurs no extra cost.</p>
<pre><code class="language-typescript">import { journey, step, expect } from '@elastic/synthetics';
import { TLS_TARGET_HOSTS } from '../../helpers/tlsTargetHosts.tls-browser.generated';
import { fetchCertInfo, checkCertTrusted, logCertInfo } from '../../helpers/tls';

for (const { host, criticality, assertionText, assertionSelector } of TLS_TARGET_HOSTS) {
  journey(
    { name: `TLS Browser Check - ${host}`, tags: criticality ? [`criticality:${criticality}`] : [] },
    ({ page }) =&gt; {
      step(`TLS Validation for ${host}:443`, async () =&gt; {
        // Route-stub: gives Synthetics UI the actual hostname instead of about:blank.
        await page.route('**/*', route =&gt; route.fulfill({ status: 200, body: 'TLS check context' }));
        await page.goto(`https://${host}`, { waitUntil: 'commit' });

        const cachedCert = await fetchCertInfo(host, 443);
        const trusted = await checkCertTrusted(host, 443);
        logCertInfo(host, 443, cachedCert); // emits TLS_CERT stdout line for ingest pipeline

        expect(cachedCert.sha256).toMatch(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/);
        expect(cachedCert.validTo.getTime(), `Certificate expired: ${host}`).toBeGreaterThan(Date.now());
        expect(trusted, `CA untrusted: ${host}`).toBe(true);
      });

      step(`Navigate to ${host} and verify page content`, async () =&gt; {
        await page.context().route('**', route =&gt; route.continue());
        const response = await page.goto(`https://${host}`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
        expect(response).not.toBeNull();
        if (assertionText &amp;&amp; assertionSelector) {
          await expect(page.locator(assertionSelector)).toContainText(assertionText);
        }
      });
    }
  );
}
</code></pre>
<p>Both successful <em>and</em> failed runs index <code>tls.server.x509.*</code> into <code>synthetics-*</code>; the alert rule reads both.</p>
<h3>The ingest pipeline (don't skip this)</h3>
<p><code>logCertInfo()</code> emits a structured <code>TLS_CERT</code> stdout line per check. Without an ingest pipeline, that lands under <code>synthetics.payload.message</code> and the Kibana TLS Summary card stays empty even when journeys are succeeding. Most certificate-monitoring tutorials skip this step; it caught me out the first time.</p>
<pre><code class="language-bash">curl -X PUT -H &quot;Content-Type: application/json&quot; \
  -u &quot;elastic:${ELASTIC_PASSWORD}&quot; \
  &quot;https://&lt;YOUR_CLUSTER_HOST&gt;:9243/_ingest/pipeline/synthetics-browser%40custom&quot; \
  --data-binary @docs/ingest-pipeline-synthetics-browser.json
</code></pre>
<p>The pipeline parses the embedded JSON into <code>tls.server.x509</code> and <code>tls.server.hash</code>, stripping colons from the fingerprint hex to match the UI's expected format.</p>
<h3>Deploying monitors via CI/CD</h3>
<pre><code class="language-bash">export KIBANA_URL=&quot;https://your-deployment.kb.us-east-1.aws.elastic-cloud.com&quot;
export SYNTHETICS_API_KEY=&quot;&lt;your-kibana-api-key&gt;&quot;
npm run push
</code></pre>
<p>Adding a new service is a CSV row in a PR, not a Kibana form fill; the same monitors-as-code idea covered for Logstash in <a href="https://www.elastic.co/observability-labs/blog/logstash-pipeline-management-configuration-gitops">Logstash Pipeline Management &amp; Configuration with GitOps</a>. The repo's GitHub Actions workflow runs <code>tsc --noEmit</code>, <code>test:dry</code>, and <code>test:unit</code> on every PR (none of which need network access, so the loop catches journey breakage before merge).</p>
<h3>Osquery host certificate inventory</h3>
<p>Browser synthetics only sees certificates bound to active web listeners. Backend databases, internal gRPC services, and message queue brokers don't surface in any Playwright test. The Osquery pack queries each host's certificate store daily and writes to <code>logs-osquery_manager.result*</code> with these ECS mappings:</p>
<pre><code class="language-json">&quot;ecs_mapping&quot;: {
  &quot;tls.server.x509.subject.common_name&quot;: { &quot;field&quot;: &quot;common_name&quot; },
  &quot;tls.server.x509.not_before&quot;:          { &quot;field&quot;: &quot;not_valid_before&quot; },
  &quot;tls.server.x509.not_after&quot;:           { &quot;field&quot;: &quot;not_valid_after&quot; },
  &quot;tls.server.hash.sha1&quot;:                { &quot;field&quot;: &quot;sha1&quot; },
  &quot;file.path&quot;:                           { &quot;field&quot;: &quot;path&quot; }
}
</code></pre>
<p>Without ECS mapping, certificate fields land in proprietary <code>osquery.*</code> columns. With it, <code>tls.server.x509.subject.common_name</code> is directly matchable against the Synthetics alert payload, which is what the workflow's host-discovery step depends on.</p>
<blockquote>
<p><strong>CN matching is necessary but not sufficient.</strong> Modern certificates carry many SANs and wildcard subjects. The host-discovery query joins on <code>common_name</code> because that is what the alert payload exposes most cleanly; in environments with heavy wildcard usage (<code>*.svc.cluster.local</code>, <code>*.internal</code>) extend the join to also match against <code>tls.server.x509.alternative_names</code> and filter out shared wildcards renewed centrally — otherwise one Let's-Encrypt-backed wildcard renewal will fan out the workflow to every host in the cluster on the same day.</p>
</blockquote>
<hr />
<h2>Think: alerting on Synthetics data</h2>
<p>The alert rule runs against <code>synthetics-*</code>, not Osquery. Synthetics captures the certificate as the client actually sees it, including intermediate-chain issues that host keystores can't detect. Osquery is consulted later, <em>inside</em> the workflow to find which hosts to act on.</p>
<p>Create an <strong>Elasticsearch query rule</strong> with this ES|QL (full file: <code>docs/elastic-samples/alerts/01-certificate-approaching-30d-expiring.md</code> — note the repo file uses a 30-day window for stricter alerting; the 90-day version below matches my own preferred default):</p>
<pre><code class="language-esql">FROM synthetics-*,*:synthetics-*
| WHERE tls.server.x509.not_before IS NOT NULL
    AND tls.server.x509.not_after IS NOT NULL
    AND tls.server.x509.subject.common_name IS NOT NULL
| STATS
    record_count = COUNT(*),
    tls.server.x509.not_after = MAX(tls.server.x509.not_after),
    tls.server.x509.not_before = MIN(tls.server.x509.not_before),
    monitor.name = VALUES(monitor.name),
    @timestamp = MAX(@timestamp)
  BY tls.server.x509.subject.common_name, tags
| WHERE NOW() + 90d &gt; tls.server.x509.not_after
    OR tls.server.x509.not_before &gt; NOW()
| EVAL days_until_expiry = DATE_DIFF(&quot;days&quot;,
    tls.server.x509.not_before,
    tls.server.x509.not_after)
| KEEP @timestamp, tls.server.x509.subject.common_name,
       tls.server.x509.not_before, tls.server.x509.not_after,
       monitor.name, tags, days_until_expiry
| SORT @timestamp
| LIMIT 1000
</code></pre>
<p>The <code>STATS BY common_name, tags</code> aggregation deduplicates to one row per certificate per criticality tag, so the rule fires <em>once per expiring cert</em>, not once per monitor execution per location. <code>MAX(not_after)</code> and <code>MIN(not_before)</code> pick the definitive validity window across all observed documents.</p>
<p>Two tuning notes worth flagging:</p>
<ul>
<li>ACME certificates (90-day validity) will sit at <code>days_until_expiry</code> near 90 every check cycle. Run a separate rule with a tighter threshold (&lt; 14 days) and a different escalation path for them.</li>
<li>For cross-cluster index patterns (<code>*:synthetics-*</code>), confirm <code>tags</code> is populated consistently across clusters before relying on it for routing.</li>
</ul>
<hr />
<h2>Act: smart certificate rotation escalation and remediation</h2>
<p>The full workflow lives at <a href="https://github.com/adrianchen-es/aiops-synthetics-lab/blob/main/docs/elastic-samples/workflows/01-smart-certificate-rotation-escalation-remediation.yaml"><code>docs/elastic-samples/workflows/01-smart-certificate-rotation-escalation-remediation.yaml</code></a>. Copy it into Kibana → Stack Management → Workflows, swap connector IDs, and you have the picture below running. What follows is just the patterns you can't intuit by skimming the YAML.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/cert-decision-tree.svg" alt="Decision tree for the Smart Escalation and Remediation workflow" /></p>
<p><em>Figure 4: Every leaf has a contract — auto-rotate, ticket, soft-notify, or page. The workflow never silently drops a certificate.</em>
The image below shows how smart escalation and remediation operate. Beneath it, the reasoning for each stage is explained.
<img src="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/cert-remediation-sequence.svg" alt="Alert-to-remediation sequence: pre-flight, host discovery, rotation or ticket, escalation, and on-demand canary impact validation" /></p>
<p><em>Figure 5: The same logic on the time axis instead of the decision axis.</em></p>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Behavior</th>
<th>Why it's interesting</th>
</tr>
</thead>
<tbody>
<tr>
<td>1. Pre-flight</td>
<td>Capture <code>severity</code> + business-hours flag</td>
<td>In 9.3, <code>console</code> steps act as expression evaluators; in 9.4, <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/data"><code>data.set</code></a> is the named, testable equivalent</td>
</tr>
<tr>
<td>2. Host discovery</td>
<td>ES|QL on <code>logs-osquery_manager.*</code> matching the alert's CN</td>
<td>Cross-source join: alert from Synthetics, target list from Osquery</td>
</tr>
<tr>
<td>3. Guard</td>
<td>Email + abort if no Osquery host matches</td>
<td>Cert visible on the wire but not in any keystore = <em>real finding</em></td>
</tr>
<tr>
<td>4. Branch</td>
<td><code>&lt; 7 days</code> → rotate; otherwise → Jira ticket</td>
<td>Single threshold, no fuzzy logic, no ambiguity</td>
</tr>
<tr>
<td>5. Rotate</td>
<td>Freshness check, then Ansible webhook + Slack</td>
<td>Circuit breaker before action — see snippet below</td>
</tr>
<tr>
<td>6. Escalate</td>
<td>Soft email or hard PagerDuty page when rotation can't proceed safely</td>
<td>Severity + business hours decide the channel</td>
</tr>
</tbody>
</table>
<h4>The patterns that matter</h4>
<p><strong>Setting workflow variables.</strong> The pre-flight steps compute and store values that every later branch reads. In 9.3, the only way to do this was to abuse <code>console</code> steps — a Liquid template in <code>message</code> renders to a string accessible at <code>steps.&lt;name&gt;.output</code>. In 9.4, <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/data"><code>data.set</code></a> makes this an explicit, named, testable operation with typed output fields.</p>
<p><strong>9.3 — <code>console</code> as expression evaluator (compatible with 9.3 and 9.4; not recommended for new workflows):</strong></p>
<pre><code class="language-yaml">- name: business_hours_check
  type: console
  with:
    message: |-
      {%- assign hour = &quot;now&quot; | date: &quot;%H&quot;, &quot;Australia/Sydney&quot; | plus: 0 %}
      {%- if hour &gt;= 8 and hour &lt; 17 %}true{%- else %}false{%- endif %}
</code></pre>
<p><strong>9.4 — <code>data.set</code> (preferred for new workflows):</strong></p>
<pre><code class="language-yaml">- name: business_hours_check
  type: data.set
  with:
    is_business_hours: |-
      {%- assign hour = &quot;now&quot; | date: &quot;%H&quot;, &quot;Australia/Sydney&quot; | plus: 0 -%}
      {%- if hour &gt;= 8 and hour &lt; 17 %}true{%- else %}false{%- endif -%}
</code></pre>
<p>The <code>data.set</code> output is accessible at <code>steps.business_hours_check.output.is_business_hours</code>, a named field rather than a raw string. The same upgrade applies to the <code>severity_mapping</code> and <code>pagerduty_criticality_mapping</code> steps in the full workflow YAML. Swap the timezone to suit your team.</p>
<p><strong>Cross-source host discovery.</strong> The alert fires on Synthetics; this query asks Osquery <em>which hosts carry the expiring CN</em>:</p>
<pre><code class="language-yaml">- name: find_affected_hosts
  type: elasticsearch.esql.query
  with:
    query: |
      FROM logs-osquery_manager.result-*
      | WHERE tls.server.x509.subject.common_name == &quot;{{ event.alerts[0]['tls.server.x509.subject.common_name'][0] }}&quot;
        AND `file.path` != &quot;LocalMachine\\Certificate Enrollment Requests&quot;
      | EVAL days_until_expiry = DATE_DIFF(&quot;days&quot;, tls.server.x509.not_before, tls.server.x509.not_after)
      | KEEP host.hostname, tls.server.x509.subject.common_name,
             tls.server.x509.not_before, tls.server.x509.not_after,
             file.path, days_until_expiry
    format: json
</code></pre>
<p>Output columns are accessed positionally below: <code>[0]</code> host.hostname, <code>[5]</code> days_until_expiry. The Windows <code>Certificate Enrollment Requests</code> exclusion drops <em>pending</em> renewal requests from the keystore — they generate false matches mid-renewal.</p>
<p><strong>The circuit breaker.</strong> Don't act on a host that can't currently observe itself:</p>
<pre><code class="language-yaml">- name: check_host_freshness
  type: elasticsearch.esql.query
  with:
    query: |
      FROM metrics-system.cpu-*
      | WHERE host.name == &quot;{{ steps.find_affected_hosts.output.values[0][0] }}&quot;
      | STATS latest_metric = MAX(@timestamp)
      | EVAL latency_sec = (TO_LONG(NOW()) - TO_LONG(latest_metric)) / 1000
      | LIMIT 1

- name: remediation_routing_logic
  type: if
  condition: 'steps.check_host_freshness.output.values[1] &lt;= 300'
  steps:
    - name: trigger_ansible_rotation
      type: http
      with:
        url: &quot;{{consts.ansible_webhook}}&quot;
        method: POST
        body: { extra_vars: { target_host: &quot;{{ steps.find_affected_hosts.output.values[0][0] }}&quot; } }
        headers: { Authorization: &quot;Bearer {{consts.ansible_token}}&quot; }
</code></pre>
<p>This is the <em>trust deficit</em> circuit breaker made concrete. My first version of this workflow didn't have the freshness check, we cheerfully fired Ansible jobs at hosts that had already left the building. Tower returned 200 because the job <em>queued</em>, the workflow declared success, and the certificate never actually rotated. Two lines of YAML and one postmortem you don't have to write.</p>
<p>Ansible Tower is illustrative — <code>type: http</code> works with any webhook-addressable executor: Rundeck, AWX, Salt, GitHub Actions <code>workflow_dispatch</code>, a thin internal service in front of <code>kubectl rollout restart</code>. The pattern that matters is the health check <em>before</em> the action.</p>
<blockquote>
<p>The <code>ansible_token</code> literal in <code>consts:</code> is for prose readability. In production, inject the credential via Workflows' secret handling (or load it from a referenced connector) so it never lands in version-controlled YAML.</p>
</blockquote>
<p><strong>Severity-aware escalation when the circuit breaker fires.</strong> Low severity outside business hours gets a non-urgent email; everything else pages, with severity translated to PagerDuty's vocabulary. Same 9.3/9.4 pattern as above:</p>
<p><strong>9.3:</strong></p>
<pre><code class="language-yaml">- name: pagerduty_criticality_mapping
  type: console
  with:
    message: |-
      {%- assign sev = steps.severity_mapping.output -%}
      {%- case sev -%}
        {%- when &quot;critical&quot; -%}critical
        {%- when &quot;high&quot;     -%}error
        {%- when &quot;medium&quot;   -%}warning
        {%- else            -%}info
      {%- endcase -%}
</code></pre>
<p><strong>9.4:</strong></p>
<pre><code class="language-yaml">- name: pagerduty_criticality_mapping
  type: data.set
  with:
    severity: |-
      {%- assign sev = steps.severity_mapping.output.severity -%}
      {%- case sev -%}
        {%- when &quot;critical&quot; -%}critical
        {%- when &quot;high&quot;     -%}error
        {%- when &quot;medium&quot;   -%}warning
        {%- else            -%}info
      {%- endcase -%}
</code></pre>
<p>The mapping step has to run <em>before</em> <code>hard_notification</code> references its output; keep that ordering if you refactor. In 9.4, the <code>data.set</code> output is referenced by field name (<code>steps.severity_mapping.output.severity</code>); in 9.3, the <code>console</code> step exposes a raw string at <code>steps.severity_mapping.output</code>.</p>
<hr />
<h2>Verify: closing the loop</h2>
<p>A self-healing system that takes an action and walks away is not self-healing: it's just hopeful. <strong>Verify</strong> is the phase that distinguishes a closed loop from a fire-and-forget script, and it's where most &quot;automated remediation&quot; implementations quietly fall short.</p>
<p>For certificates, verification has two natural anchors. The next Synthetics interval (1–10 minutes after rotation) re-reads the chain — the new <code>not_after</code> and fingerprint land in <code>synthetics-*</code> and the alert rule simply stops matching that CN. <em>No alert is the right outcome.</em> For deeper confidence, the canary <code>curl_certificate</code> workflow confirms that the <em>fleet</em> — not just the public endpoint — has the new cert. Add the following block at the end of the rotation success path:</p>
<pre><code class="language-yaml">- name: wait_for_rotation
  type: wait
  with:
    duration: 120s

- name: verify_rotation
  type: elasticsearch.esql.query
  with:
    query: |
      FROM synthetics-*
      | WHERE tls.server.x509.subject.common_name
          == &quot;{{ event.alerts[0]['tls.server.x509.subject.common_name'][0] }}&quot;
        AND @timestamp &gt; NOW() - 5 minutes
      | STATS new_not_after = MAX(tls.server.x509.not_after),
              new_fingerprint = VALUES(tls.server.hash.sha256)
      | EVAL days_remaining = DATE_DIFF(&quot;days&quot;, NOW(), new_not_after)

- name: rotation_outcome
  type: if
  condition: &quot;${{ steps.verify_rotation.output.values[0][2] &gt; 30 }}&quot;
  steps:
    - name: notify_verified
      type: slack
      connector-id: &quot;slack-sre-channel-uuid&quot;
      with:
        message: &quot;✅ Rotation verified. {{ steps.verify_rotation.output.values[0][2] }} days remaining.&quot;
  else:
    - name: notify_verification_failed
      type: pagerduty
      connector-id: &quot;&lt;your-pagerduty-connector-uuid&gt;&quot;
      with:
        eventAction: &quot;trigger&quot;
        severity: &quot;error&quot;
        summary: &quot;Rotation triggered but Synthetics still shows old cert. Investigate Ansible job and DNS/cache.&quot;
</code></pre>
<p>The verify query reads from the same source the alert rule reads from, so the two definitions can never drift. The check is numeric (<code>days_remaining &gt; 30</code>), nothing to misinterpret. And a failed verification escalates <em>harder</em> than the original: a cert that was about to expire and silently failed to rotate is the more dangerous state, because automation has already swallowed the warning a human would otherwise have seen.</p>
<p>The 120-second pause is conservative. For ACME certs renewed by a sidecar, the next monitor interval (often under a minute) is enough. For Ansible-driven PKI rotations that touch a reverse-proxy reload, two minutes feels about right in my experience; long enough not to chase a stale cache, short enough that on-call doesn't go to bed thinking the rotation worked.</p>
<p>To rehearse the whole loop end-to-end before pointing it at production, the repo's <code>local-lab/</code> directory ships a Docker Compose setup that issues a <em>one-day</em> self-signed cert and serves it from Nginx or Apache. Renew the cert, restart the container, and watch Synthetics pick up the new fingerprint within one monitor cycle — same shape as a real rotation, with no production risk.</p>
<blockquote>
<p>Skip Verify and you have built an open loop dressed up as a closed one. The <strong>trust deficit</strong> returns the first time a silent rotation failure becomes a 4 AM page.</p>
</blockquote>
<hr />
<h2>Impact validation: the canary workflow</h2>
<p>The combined workflow handles the predictable case. Some incidents fall outside the predictable case: a Synthetic monitor failing with a TLS handshake timeout where the chain is not obvious, or a situation where you want to confirm blast radius before committing to rotation. For these, an Osquery <code>curl_certificate</code> live query gives a client-side view of the TLS chain from the perspective of <em>machines that actually communicate with the target</em>. A reverse proxy can hold a valid public cert while a downstream internal host serves an expired CA-signed cert that breaks microservice calls: invisible to endpoint polling, fully visible from internal canary hosts.</p>
<p>Two workflow files are in the repo: <a href="https://github.com/adrianchen-es/aiops-synthetics-lab/blob/main/docs/elastic-samples/workflows/02-canary-certificate-impact-check.yaml"><code>02-canary-certificate-impact-check.yaml</code></a> (9.3-compatible) and <a href="https://github.com/adrianchen-es/aiops-synthetics-lab/blob/main/docs/elastic-samples/workflows/02-canary-certificate-impact-check-9.4.yaml"><code>02-canary-certificate-impact-check-9.4.yaml</code></a> (GA default for 9.4+). Two ways to invoke either:</p>
<ul>
<li><strong>Manually</strong> in Kibana with <code>target_host</code> and <code>target_port</code> inputs.</li>
<li><strong>Autonomously</strong> as a registered Agent Builder tool, used by the SRE Certificate Agent during triage — same registration pattern as <a href="https://www.elastic.co/observability-labs/blog/troubleshoot-kubernetes-pod-restarts-oomkilled-elastic-agent-builder">How to Troubleshoot Kubernetes Pod Restarts &amp; OOMKilled Events with Agent Builder</a>.</li>
</ul>
<p>The interesting moves inside the YAML are: dispatch a <code>curl_certificate</code> live query with <code>agent_all: true</code>, poll the live-query API until status is <code>completed</code>, then aggregate the asynchronous results with one ES|QL pass. The polling step is where 9.3 and 9.4 differ most visibly.</p>
<p><strong>9.3 — <code>foreach</code> + intentional failure workaround:</strong></p>
<pre><code class="language-yaml">consts:
  items: [1]   # single-element list; foreach runs once per retry attempt

steps:
  - name: while_not_workaround_loop
    type: foreach
    foreach: &quot;${{consts.items}}&quot;
    steps:
      - name: osquery_check_query_completion
        type: kibana.request
        with:
          method: GET
          path: /api/osquery/live_queries/{{ steps.osquery_check_host.output.data.action_id }}
      - name: wait_10s
        type: wait
        with:
          duration: 10s
      - name: retry
        type: http
        if: &quot;${{ steps.osquery_check_query_completion.output.data.status != 'completed' }}&quot;
        with:
          url: &quot;https://httpbin.org/status/404&quot;   # deliberate 404 triggers on-failure
    on-failure:
      retry:
        max-attempts: 9
</code></pre>
<p><strong>9.4 — native <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/flow-control-steps#while"><code>while</code> loop</a> with <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/data"><code>data.set</code></a>:</strong></p>
<pre><code class="language-yaml">
- name: poll_osquery_completion
    type: while
    condition: &quot;${{ steps.osquery_poll_state.output.query_status != 'completed' }}&quot;
    max-iterations:
      limit: 10
      on-limit: fail
    steps:
      - name: wait_10s
        type: wait
        with:
          duration: 10s
      - name: check_query_completion
        type: kibana.request
        with:
          method: GET
          path: /api/osquery/live_queries/{{ steps.osquery_check_host.output.data.action_id }}
      - name: osquery_poll_state
        type: data.set
        with:
          query_status: &quot;{{ steps.check_query_completion.output.data.status }}&quot;
</code></pre>
<p>See the <a href="https://www.elastic.co/docs/explore-analyze/workflows/authoring-techniques/migrate-from-9-3">9.3 → 9.4 migration guide</a> for a full list of changes. The ES|QL aggregation step is identical in both versions:</p>
<pre><code class="language-esql">FROM logs-osquery_manager.result*
| WHERE action_id == &quot;{{ steps.osquery_check_host.output.data.queries[0].action_id }}&quot;
| WHERE NOW() &gt; tls.server.x509.not_after
| STATS expired_canary_hosts = COUNT_DISTINCT(agent.name)
    BY certificate_validity_duration = DATE_DIFF(&quot;day&quot;,
        tls.server.x509.not_before,
        tls.server.x509.not_after)
</code></pre>
<p><code>expired_canary_hosts</code> is the count of agents that confirmed expiry. <code>certificate_validity_duration &gt; 90</code> distinguishes long-lived PKI from short-lived ACME, which the workflow uses to route the response — replace the <code>console</code> placeholders in the YAML with Slack / PagerDuty / a follow-on remediation step as preferred.</p>
<h3>Registering as an agent tool</h3>
<p>Register the workflow as a tool so any Agent Builder agent can invoke it during triage:</p>
<pre><code class="language-json">POST kbn:/api/agent_builder/tools
{
  &quot;id&quot;: &quot;o11y.canary_certificate_check&quot;,
  &quot;type&quot;: &quot;workflow&quot;,
  &quot;description&quot;: &quot;Queries curl_certificate from all fleet agents to confirm real TLS impact for a given hostname and port. Returns count of affected canary hosts and certificate type (PKI vs ACME).&quot;,
  &quot;tags&quot;: [&quot;observability&quot;, &quot;certificates&quot;],
  &quot;configuration&quot;: {
    &quot;workflow_id&quot;: &quot;&lt;your-osquery-curl-workflow-id&gt;&quot;,
    &quot;wait_for_completion&quot;: true
  }
}
</code></pre>
<h3>Packaging as a skill (9.4+)</h3>
<p>Elastic 9.4 introduces <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/skills">skills</a>, a higher-level abstraction over individual tools. Where a tool is a single discrete operation (run a query, call a webhook), a skill bundles instructions, tools, and reference context into a reusable capability pack that an agent loads selectively rather than holding in the system prompt at all times.</p>
<p>For certificate triage, a skill lets you combine the canary impact check tool with domain-specific instructions, for example, how to interpret PKI vs ACME results, when to escalate vs when to redirect to DNS, and which follow-on workflows to invoke. The agent selects the skill automatically when the conversation context signals a TLS problem, or users invoke it directly with a slash command (<code>/cert-triage</code>).</p>
<p>Skills are managed through the skill library in Agent Builder and are available to any agent in the workspace: one definition, shared across the SRE agent, the on-call agent, and any future agents that need certificate expertise. Retrieve available skills with <code>GET /api/agent_builder/skills</code>.</p>
<p>A typical agent-driven triage with the skill loaded:</p>
<pre><code class="language-text">SRE: &quot;The Synthetics monitor for blueprint-portal is failing. Investigate.&quot;

Agent: [Reviews synthetics logs — TLS handshake error on blueprint-portal.internal]
Agent: [Invokes o11y.canary_certificate_check for blueprint-portal.internal:443]

Canary result: expired intermediate cert confirmed on 3 of 12 agents — PKI class (365-day cert)

Agent: &quot;The Synthetic failure is caused by an expired intermediate CA on the downstream
gateway. Three canary hosts confirm. The leaf cert is valid — this is a CA renewal gap on
the internal chain, not a service certificate expiry.&quot;

SRE: &quot;Execute the proxy cache flush workflow.&quot;
Agent: [Invokes proxy flush workflow via MCP]
Agent: &quot;Flush confirmed. Monitor should recover within one check interval.&quot;
</code></pre>
<p>When the canary check comes back clean, the agent redirects to DNS, routing, or upstream service health without the SRE needing to query anything by hand.</p>
<hr />
<h2>Operational considerations</h2>
<p><strong>Observability of the workflow itself.</strong> The question I get asked once people see this pattern is the same: <em>&quot;how do you know the workflow is still doing what you think?&quot;</em> The Workflows run history in Kibana shows every execution, status, and step output. And the clearest sign that the loop is <em>closing</em>, straight from the alerts index:</p>
<pre><code class="language-esql">// Certificates that fired the alert more than once in the last 14 days —
// strong signal that rotation is failing silently and the loop is open
FROM .alerts-observability.metrics.alerts-default
| WHERE @timestamp &gt; NOW() - 14 days
  AND kibana.alert.rule.name == &quot;Certificate Expiry&quot;
| STATS fires = COUNT(*) BY tls.server.x509.subject.common_name
| WHERE fires &gt; 1
| SORT fires DESC
</code></pre>
<p>Pin that query somewhere visible. A CN appearing more than once means the workflow is acting, but the action isn't landing; exactly the failure mode this architecture exists to surface. The companion meta-alert query is checked in at <code>docs/elastic-samples/alerts/02-certificate-workflow-verification.md</code>.</p>
<p><strong>Criticality context for backend hosts.</strong> The CSV <code>tags</code> field only covers endpoints with active Synthetic monitors. For Osquery-discovered certificates on backend services with no monitor, add a <code>LOOKUP JOIN enterprise_cmdb ON host.name</code> step before <code>remediate_or_notify</code>. Two patterns keep the CMDB index current: pull (Logstash sync of CMDB exports) or push (the workflow POSTs back via ServiceNow webhook when Osquery surfaces a host that isn't in the CMDB; that's a real finding worth investigating).</p>
<p><strong>Rehearse end-to-end before production.</strong> The repo's <code>local-lab/</code> directory issues a 1-day self-signed cert, serves it via Nginx (port 8443) or Apache (port 8444), and includes a <code>renew-tls-certs.sh</code> script. Run the loop against it for a day or two before pointing the workflow at services that matter.</p>
<hr />
<h2>What success looks like</h2>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Healthy state</th>
</tr>
</thead>
<tbody>
<tr>
<td>Repeat-fires per CN over 14 days</td>
<td>Near zero — every alert is acted on and verified within one cycle</td>
</tr>
<tr>
<td>Workflow <code>success</code> rate</td>
<td>Dominant; <code>failed</code> runs are mostly host-stale escalations, not bugs</td>
</tr>
<tr>
<td>Auto-rotation share of total runs</td>
<td>Increases as the team trusts the ≤ 7-day window</td>
</tr>
<tr>
<td>Manual Jira tickets</td>
<td>Drops as more cert classes graduate from &quot;ticket&quot; to &quot;auto-rotate&quot;</td>
</tr>
<tr>
<td>4 AM cert pages</td>
<td>Approaches zero. If they don't, the loop isn't closing. Start with the repeat-fires query.</td>
</tr>
</tbody>
</table>
<p>The single highest-value metric is the last one. A successful self-healing build is one that no human notices for months at a time.</p>
<hr />
<h2>Repository structure</h2>
<p><img src="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/cert-repo-structure.svg" alt="Repository structure and key entry points for the aiops-synthetics-lab project" /></p>
<p><em>Figure 6: CSV in, monitors out. Sample workflow YAML, alert ES|QL, ingest pipeline, and a short-lived-cert local lab are all colocated and version-controlled together.</em></p>
<p>Full reference at <a href="https://github.com/adrianchen-es/aiops-synthetics-lab">github.com/adrianchen-es/aiops-synthetics-lab</a>.</p>
<hr />
<h2>Next steps</h2>
<ol>
<li>Clone the repo, edit <code>journeys/tls-browser/tls-target-hosts.csv</code> with your endpoints and criticality tags.</li>
<li>Install the ingest pipeline once: <code>PUT _ingest/pipeline/synthetics-browser@custom</code>.</li>
<li>Run <code>npm run test:demos</code> against <code>revoked.badssl.com</code> to confirm Sense and the ingest pipeline are wired correctly. Optionally point the journey at the <code>local-lab/</code> Nginx (<code>https://127.0.0.1:8443</code>) to rehearse the full loop with 1-day certificates.</li>
<li>Push monitors with <code>npm run push</code>.</li>
<li>Deploy the Osquery pack via the Kibana API.</li>
<li>Paste the ES|QL from <code>docs/elastic-samples/alerts/01-…md</code> into a Kibana ES|QL rule.</li>
<li>Import <code>docs/elastic-samples/workflows/01-…yaml</code> as a Kibana Workflow and configure connector IDs. Add the Verify block before enabling auto-rotation in production.</li>
<li>Optionally import <code>docs/elastic-samples/workflows/02-…yaml</code> and register it as an Agent Builder tool.</li>
</ol>
<p>Try it on <a href="https://cloud.elastic.co/registration">Elastic Cloud Serverless</a> or <a href="https://cloud.elastic.co/registration">Elastic Cloud</a>.</p>
<hr />
<h2>Frequently asked questions</h2>
<p><strong>Why do TLS certificates keep expiring in production if the expiry date is known in advance?</strong></p>
<p>The expiry date is known, but acting on it requires a reliable chain from detection to rotation — and that chain typically runs through spreadsheets, manual calendar reminders, and whoever happens to notice first. Automation closes that gap: Elastic Synthetics and Osquery provide continuous visibility into expiry state, while Elastic Workflows manages rotation and verification without manual intervention.</p>
<p><strong>How is monitoring TLS certificates with Elastic Synthetics different from a simple cron job or alerting rule?</strong></p>
<p>A Synthetics journey captures the full certificate chain as the client actually sees it, including intermediate CA issues that a host-side check misses entirely. It also maps results into <code>tls.server.x509.*</code> ECS fields, so the same data feeds both the alert rule and the workflow's host-discovery query — no translation layer, no schema drift between detection and remediation.</p>
<p><strong>How do I make sure automated certificate rotation actually worked and didn't just queue a job?</strong></p>
<p>After triggering rotation, the Elastic Workflow waits 120 seconds and queries <code>synthetics-*</code> with ES|QL to check whether the new <code>not_after</code> date is more than 30 days out. If the old certificate is still present, the workflow escalates to PagerDuty at higher severity than the original alert — because a silent rotation failure is more dangerous than the expiry warning it consumed.</p>
<p><strong>What happens if Ansible fires a rotation job at a host that's already been decommissioned?</strong></p>
<p>The workflow includes a circuit-breaker step that queries <code>metrics-system.cpu-*</code> before triggering Ansible. If the host hasn't reported telemetry in the last 300 seconds, the rotation is skipped and an escalation is sent instead. Without this check, the Ansible Tower job queues and returns 200, the workflow declares success, and the certificate never actually rotates.</p>
<p><strong>When should I use the Osquery <code>curl_certificate</code> canary workflow instead of the main rotation workflow?</strong></p>
<p>The main rotation workflow handles predictable expiry detected by Synthetics. The canary workflow is for triage when the failure mode is unclear — for example, a TLS handshake timeout where the root cause isn't obvious. It dispatches a <code>curl_certificate</code> live query to all fleet agents simultaneously and tells you which machines confirm the expiry, distinguishing a public endpoint failure from an internal chain failure that Synthetics can't see.</p>
<p><strong>Does this pipeline require Elastic Synthetics monitors for every certificate I want to cover?</strong></p>
<p>No. The Osquery component independently inventories the local certificate store on every enrolled host daily, surfacing certificates used by backend databases, gRPC services, and message brokers that have no HTTP endpoint. Synthetics covers what clients see from the outside; Osquery covers what hosts hold internally. Both write to <code>tls.server.x509.*</code> so the remediation workflow can join them on common name without a translation layer.</p>
<p><strong>What are the limitations of joining Synthetics and Osquery data on common name?</strong></p>
<p>Common name matching works well for simple certificate topologies. In environments with heavy wildcard usage (<code>*.svc.cluster.local</code>, <code>*.internal</code>), a single Let's Encrypt wildcard renewal can fan out the remediation workflow to every host in the cluster. The fix is to extend the join to <code>tls.server.x509.alternative_names</code> and filter out centrally managed wildcards before triggering per-host rotation.</p>
<p><strong>What version of the Elastic Stack do I need to use Elastic Workflows for certificate monitoring?</strong></p>
<p>Elastic Workflows is generally available in Elastic Stack 9.4 and was available as a Technical Preview in 9.3. The ES|QL <code>LOOKUP JOIN</code> used for host discovery requires 9.1 or later. For new deployments, target 9.4 to access the native <code>while</code> loop and <code>data.set</code> steps, which eliminate the workarounds required in 9.3.</p>
<p><strong>What is the difference between a tool and a skill in Elastic Agent Builder?</strong></p>
<p>A tool is a single discrete operation — run a query, call a webhook, trigger a workflow. A skill bundles instructions, tools, and reference context into a reusable capability pack that an agent loads selectively rather than keeping in the system prompt at all times. For certificate triage, registering the canary impact check as a tool gives an agent one action; packaging it as a skill gives the agent the context to interpret results, decide when to escalate, and invoke follow-on workflows automatically.</p>
<hr />
<h2>Related reading</h2>
<ul>
<li><a href="https://www.elastic.co/observability-labs/blog/aiops-remediation-elastic-worklfows">Automated Reliability: The Architecture of Self-Healing Enterprises</a> — the inspiration for this build. Covers the <em>why</em>: the remediation gap, the <strong>trust deficit</strong>, and the <strong>sense → shink → sct → verify</strong> loop this implementation realizes.</li>
<li><a href="https://www.elastic.co/observability-labs/blog/troubleshoot-kubernetes-pod-restarts-oomkilled-elastic-agent-builder">How to Troubleshoot Kubernetes Pod Restarts &amp; OOMKilled Events with Agent Builder</a> — a directly comparable Agent Builder triage pattern; the SRE Certificate Agent here uses the same registration model.</li>
<li><a href="https://www.elastic.co/observability-labs/blog/agentic-cicd-kubernetes-mcp-server">Agentic CI/CD: Kubernetes Deployment Gates with Elastic MCP Server</a> — extends the workflow-as-tool pattern to deployment pipelines; relevant if you want to gate releases on certificate health.</li>
<li><a href="https://www.elastic.co/observability-labs/blog/elastic-ramen-agent-builder-cli">Elastic Ramen: A CLI harness for SRE investigation and remediation</a> — brings Agent Builder conversations, skills, and tools into the terminal so engineers can move from investigation to remediation in a single thread.</li>
</ul>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/certificate-monitoring-automation-elastic-workflows/certificate-monitoring-automation-elastic-workflows.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Logstash Pipeline Management & Configuration with GitOps]]></title>
            <link>https://www.elastic.co/observability-labs/blog/logstash-pipeline-management-configuration-gitops</link>
            <guid isPermaLink="false">logstash-pipeline-management-configuration-gitops</guid>
            <pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Stop treating Logstash like a black box. This guide shows you how to use GitOps to create auditable, automated, and resilient data pipelines. Eliminate config drift and boost security with this GitHub and Jenkins blueprint.]]></description>
            <content:encoded><![CDATA[<p>Is your Logstash environment a 'black box'? Are manual configuration changes leading to unexpected outages, security gaps, and countless hours spent on troubleshooting? It's time to stop treating observability infrastructure like a fragile art project. This blog post delivers a strategic blueprint for taming your Logstash pipelines, transforming them into a version-controlled, automated, and auditable asset. By adopting a GitOps approach, you can eliminate configuration drift, empower your teams to collaborate securely and ensure your observability platform is as resilient as the systems it monitors.</p>
<h2>From Fragile Art Project to Auditable Asset: How to Tame Your Logstash Configurations with Version Control and Automation</h2>
<p>Observability ensures system health, performance, and security. Logstash drives this by processing and routing your data. But as you scale, manual configuration management becomes a bottleneck. It leads to errors, outages, and security gaps. You need a better way.</p>
<p>This blog post shows you how to manage Logstash pipelines using GitOps. You will use Git as your single source of truth and automate deployments to increase stability, security, and efficiency of your enterprise organisation’s observability infrastructure. </p>
<p>This blog post details the benefits of this methodology and provides a practical implementation model using <strong>GitHub</strong> for version control and <strong>Jenkins</strong> for Continuous Integration and Continuous Deployment (CI/CD).</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/logstash-pipeline-management-configuration-gitops/ls-pipeline-gitops-flow.png" alt="Logstash Central Pipeline GitOps flow" /></p>
<h2>The Unsung Hero: Why Logstash Remains a Cornerstone of Enterprise Data Strategy</h2>
<p>In the evolving landscape of observability and data pipelines, <strong>Logstash</strong> remains one of the most powerful and reliable components in the Elastic ecosystem. While it may not always take the spotlight, its depth of capability, flexibility, and resilience make it essential for enterprises managing complex, varied data streams. Logstash offers four main benefits:</p>
<ul>
<li>
<p><strong>Extensive Integration Support:</strong> Logstash supports a wide array of input and output plugins — including Kafka, syslog, Beats, cloud services, and databases — making it ideal for ingesting data from diverse environments and routing it across your architecture.</p>
</li>
<li>
<p><strong>Advanced Data Transformation:</strong> With rich filtering capabilities and optional Ruby scripting, Logstash enables complex enrichment, field manipulation, and conditional routing — allowing teams to standardise and prepare data early in the pipeline.</p>
</li>
<li>
<p><strong>Offloading Elasticsearch Ingest Load:</strong> The <a href="https://www.elastic.co/docs/reference/logstash/using-logstash-with-elastic-integrations"><code>elastic_integration</code></a> filter replicates ingest pipeline logic in Logstash, enabling upstream transformations that reduce processing overhead on Elasticsearch and streamline the indexing path.</p>
</li>
<li>
<p><strong>Operational Resilience with Persistent Queues:</strong> Logstash’s persistent-queue buffers data during downstream slowdowns or outages, helping smooth ingestion spikes, prevent data loss, and maintain stability under load.</p>
</li>
</ul>
<p>In modern CI/CD workflows, where automation and rapid iteration are standard, Logstash’s maturity and flexibility continue to make it a dependable choice — quietly powering the data flows that keep observability pipelines running strong.</p>
<h2>The Case for a GitOps-Driven Observability Strategy</h2>
<p>GitOps is a paradigm that applies proven DevOps best practices such as version control, collaboration, compliance, and CI/CD to infrastructure and configuration management. When applied to Logstash, this means that every pipeline configuration is treated as code—defined, versioned, reviewed, and deployed from a Git repository.</p>
<p>For enterprise environments, the adoption of a GitOps model for Logstash pipelines offers compelling advantages:</p>
<ul>
<li>
<p><strong>Enhanced Auditability and Compliance:</strong> Every pipeline modification is captured as a Git commit, creating an immutable, chronological audit trail. This provides unparalleled visibility into who made what change, when, and why, which is indispensable for meeting regulatory compliance requirements and conducting security audits.</p>
</li>
<li>
<p><strong>Improved System Stability and Reliability:</strong> The risk of deploying faulty configurations is drastically reduced. By enforcing a pull request (PR) workflow, all changes undergo peer review and automated validation <em>before</em> they are merged and deployed. In the event of an incident caused by a new configuration, a rollback is as fast and straightforward as reverting a Git commit.</p>
</li>
<li>
<p><strong>Increased Automation and Operational Efficiency:</strong> Automating the deployment lifecycle eliminates manual, error-prone configuration tasks. This frees up skilled engineers from routine operational duties, allowing them to focus on higher-value activities such as optimising data flows, improving analytics, and strengthening security postures.</p>
</li>
<li>
<p><strong>Fostered Cross-Team Collaboration:</strong> Git provides a universal and well-understood platform for collaboration. Development, Security, and Operations (DevSecOps) teams can work together seamlessly on a unified codebase. This shared ownership breaks down silos and ensures that pipeline configurations are robust, secure, and fit for purpose across the organization.</p>
</li>
</ul>
<h2>Implementation Model: GitHub and Jenkins</h2>
<p>This section details a practical framework for implementing a GitOps workflow for Logstash.</p>
<h3>1. Prerequisites</h3>
<ul>
<li>
<p>An established <strong>GitHub</strong> organisation or account.</p>
</li>
<li>
<p>A running <strong>Jenkins</strong> instance with the necessary plugins installed (e.g., Git, GitHub Integration).</p>
</li>
<li>
<p>A target <strong>Logstash</strong> environment where configurations will be deployed.</p>
</li>
<li>
<p>Working knowledge of Git, Jenkins pipelines, and Logstash configuration syntax.</p>
</li>
</ul>
<h3>2. Step 1: Establish a Centralised Git Repository</h3>
<p>The foundation of a GitOps workflow is a version-controlled repository.</p>
<ol>
<li>
<p><strong>Create a Repository:</strong> In GitHub, create a new repository (e.g., logstash-configurations). This will serve as the single source of truth for all pipeline configurations.</p>
</li>
<li>
<p><strong>Define a Directory Structure:</strong> A logical directory structure is crucial for managing configurations across different environments. A recommended structure is:</p>
</li>
</ol>
<pre><code>    /
    ├── pipelines/
    │   ├── development/
    │   │   ├── 01-input-beats.conf
    │   │   ├── 10-filter-nginx.conf
    │   │   └── 99-output-elasticsearch.conf
    │   ├── staging/
    │   │   └── ...
    │   └── production/
    │       └── ...
    └── Jenkinsfile
</code></pre>
<p>This structure clearly separates configurations by environment and allows for a modular and maintainable pipeline design.</p>
<h3>3. Step 2: Automate Deployment with a Jenkins CI/CD Pipeline</h3>
<p>The Jenkins pipeline automates validation and deployment of the configurations from Git to your Logstash instances.</p>
<ol>
<li>
<p><strong>Create a</strong> <code>Jenkinsfile</code><strong>:</strong> Add a <code>Jenkinsfile</code> to the root of your repository to define the automation pipeline. This pipeline-as-code approach ensures the deployment process itself is version-controlled.</p>
</li>
<li>
<p><strong>Define the Pipeline Stages:</strong> The pipeline should include distinct stages for checking out code, validating configurations, and deploying to the target environment.</p>
<p>A sample <code>Jenkinsfile</code> could look as follows:</p>
<pre><code>
 pipeline {
     agent any

     // Trigger the pipeline on every push to the main branch
     triggers {
         githubPush()
     }

     stages {
         stage('Checkout') {
             steps {
                 // Clone the repository
                 git 'https://github.com/your-org/logstash-configurations.git'
             }
         }

         stage('Validate Staging Configs') {
             steps {
                 // Run Logstash's built-in config test
                 // This prevents syntax errors from reaching production
                 sh 'docker run --rm -v ${WORKSPACE}/pipelines/staging:/usr/share/logstash/pipeline/ docker.elastic.co/logstash/logstash:9.3.2 logstash --config.test_and_exit'
                 sh 'docker run --rm -v ${WORKSPACE}/pipelines/staging:/usr/share/logstash/pipeline/ docker.elastic.co/logstash/logstash:9.3.2 logstash --config.test_and_exit'
             }
         }

         stage('Deploy to Staging') {
             // This stage requires Jenkins to have credentials to access the Staging server
             steps {
                 withCredentials([sshUserPrivateKey(credentialsId: 'staging-server-creds', keyFileVariable: 'KEY_FILE')]) {
                     sh '''
                         scp -i ${KEY_FILE} ${WORKSPACE}/pipelines/staging/*.conf user@staging-logstash-host:/etc/logstash/conf.d/
                         ssh -i ${KEY_FILE} user@staging-logstash-host 'sudo systemctl reload logstash'
                     '''
                 }
             }
         }

         // Optional: Add a manual approval step before deploying to production
         stage('Approval for Production') {
             steps {
                 input 'Deploy to Production?'
             }
         }

         stage('Deploy to Production') {
              steps {
                 // Similar deployment steps for the production environment
                 // using production credentials
              }
         }
     }
 }
</code></pre>
</li>
</ol>
<h3>4. The GitOps Workflow in Practice</h3>
<p>This setup enables a controlled, auditable, and automated workflow:</p>
<ol>
<li>
<p><strong>Branch Creation:</strong> An engineer creates a feature branch in Git to propose a change (e.g., <code>feature/add-syslog-input</code>).</p>
</li>
<li>
<p><strong>Configuration Change:</strong> The engineer modifies or adds a pipeline configuration file in their branch.</p>
</li>
<li>
<p><strong>Pull Request:</strong> A pull request is created in GitHub. This action can trigger automated checks in Jenkins to validate the syntax of the proposed changes.</p>
</li>
<li>
<p><strong>Peer Review:</strong> Team members review the changes for logic, security, and adherence to standards.</p>
</li>
<li>
<p><strong>Merge and Deploy:</strong> Upon approval, the PR is merged into the <code>main</code> branch. This merge automatically triggers the Jenkins pipeline, which deploys the validated configuration to the corresponding Logstash environment.</p>
</li>
</ol>
<h2>Best Practices for Enterprise Adoption</h2>
<p>To successfully implement this model at an enterprise scale, consider the following best practices:</p>
<ul>
<li>
<p><strong>Branching Strategy:</strong> Adopt a consistent branching strategy, such as GitFlow, to manage features, releases, and hotfixes in an orderly manner. Protect your <code>main</code> or <code>production</code> branches with rules that require PR reviews and passing status checks before merging.</p>
</li>
<li>
<p><strong>Scalability:</strong> For large-scale deployments with many Logstash nodes, use configuration management tools like Ansible, Puppet, or Chef within your Jenkins pipeline to orchestrate the deployment across your entire fleet.</p>
</li>
<li>
<p><strong>Fostering a GitOps Culture:</strong> Successful adoption is as much about people and processes as it is about tools. Provide training and documentation to ensure all stakeholders understand the workflow and their role within it. Emphasise the collaborative benefits and the shared responsibility for maintaining a stable and secure observability platform.</p>
</li>
<li>
<p><strong>Pipeline Observability</strong> (<em>Optional</em>): Monitoring the health and performance of your CI/CD pipelines is crucial and recommended for early detection of issues, visibility into bottlenecks, and auditability. Elastic Observability provides native support for monitoring Jenkins pipelines using the Elastic <a href="https://plugins.jenkins.io/opentelemetry/">CI/CD Observability plugin</a>.</p>
</li>
<li>
<p><strong>Secrets Management:</strong> Never hardcode sensitive information (passwords, API keys) in your configuration files. Use a secrets management tool like HashiCorp Vault or AWS Secrets Manager, and have Logstash retrieve these secrets at runtime.</p>
</li>
</ul>
<p>A sample <em>snippet</em> to retrieve the secret and store it securely in a <a href="https://www.elastic.co/docs/reference/logstash/keystore">Logstash keystore</a> could look as follows:</p>
<pre><code>```
...

	stage('Update Logstash Secret') {
    // Define the secret path and key in Vault
    def secretPath = 'secret/logstash/production'
    def secretKey = 'elasticsearch_password'
    def keystoreKey = 'ES_PWD' // The key name to be used in the Logstash keystore

    // Wrap the steps in withVault to get access to the secrets
    withVault(configuration: [url: 'http://your-vault-server:8200',
                             credentialsId: 'vault-approle-creds']) {
        
        // Retrieve the secret from Vault. The plugin makes it available as an environment variable.
        def secrets = readVault(path: secretPath, key: secretKey)
        def esPassword = secrets[secretKey]

        // Use SSH credentials to access the Logstash server
        withCredentials() {
            sh &quot;&quot;&quot;
            ssh -i ${KEY_FILE} user@logstash-host &lt;&lt;'ENDSSH'
            # Pipe the secret directly into the logstash-keystore command
            # This avoids writing the secret to disk or exposing it in the process list
            echo &quot;${esPassword}&quot; | sudo -u logstash /usr/share/logstash/bin/logstash-keystore add ${keystoreKey} --stdin

            # After updating the keystore, reload Logstash to apply the change
            sudo systemctl reload logstash
            ENDSSH
            &quot;&quot;&quot;
        }
    }
}
...

```
</code></pre>
<h2>Conclusion</h2>
<p>Adopting a GitOps approach for managing Logstash pipelines is a strategic move that aligns observability with modern DevSecOps principles. It replaces manual, opaque processes with an automated, transparent, and collaborative framework. For enterprise organisations, this leads to a more secure, resilient, and efficient observability infrastructure, empowering teams to derive maximum value from their data while minimising operational overhead and risk.</p>
<p>The above example is just a start; there’s a lot more you can do once you lay the foundation—GitOps is just the beginning. From branching automation to pipeline promotion workflows to building self-service deployment portals, the possibilities are limited only by your creativity (and maybe your CI minutes).</p>
<p>GitOps lays the foundation. To see the whole picture, you need to monitor your pipelines. <a href="https://cloud.elastic.co/registration">Start a trial</a> and try out Elastic’s <a href="https://www.elastic.co/docs/solutions/observability/cicd">CI/CD Observability solution</a> to track build health and deployment trends. It connects code changes to production behavior, giving you deep visibility into your new automated workflow.</p>
<p>Build smarter pipelines. Monitor what matters. And let your GitOps-powered observability stack become the quiet hero of your DevSecOps story. See how <a href="https://www.elastic.co/elasticsearch/streams">Streams</a> can supercharge your data engineering with the next generation of AI-powered log management &amp; log processing.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/logstash-pipeline-management-configuration-gitops/continuous-improvement.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>