Test Trust Stores for Compromised CAs

Test and patch local and CI trust stores against compromised CAs.

Introduction

30 Second Summary

A certificate warning usually feels like a roadblock when you are trying to reach a website. After a certificate compromise, that warning can prove your machine rejected a dangerous connection.

In this project, you will test your browser, terminal, and Python setup against public hosts signed by eight compromised eMudhra intermediate certificates. You will turn those checks into a local script plus a GitHub Actions workflow that catches stale trust stores.

What You'll Build

You'll watch each client reject the compromised certificates before a green GitHub Actions run proves your repository can catch an unsafe trust decision.

By the end of this project, you'll have:

  • A browser safety verdict that reveals whether your browser rejects certificates signed by the compromised intermediates.
  • A set of repeatable local checks that compare curl with Python. Certificate errors confirm that each client refuses the dangerous connection.
  • A continuous trust-store guardrail that fails your GitHub Actions workflow whenever a runner accepts one of the compromised certificates.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

You'll need a terminal with curl, Python, and a repository on GitHub. This project has no expected cost.

Before We Start

Before the hands-on checks begin, take a moment to commit to what you are testing and why the result matters.

Test Your Browser Trust Store

Your browser's trust store decides whether it accepts a certificate chain. A stale trust store can make a forged certificate appear valid.

This step gives you a browser baseline before you test other clients. You will load one incident-provided test host to record the browser's decision.

In this step, get ready to:
  • Understand why a certificate error is the safe outcome.
  • Open one incident-provided compromised-intermediate test host.
  • Record whether the browser blocks the connection or displays a page.
Choose a compromised-intermediate test host

Each incident-provided test host uses a certificate signed by one compromised intermediate. A safe browser refuses the connection because it no longer trusts that chain.

Why is an error the safe result?

This check reverses the usual pass condition. A certificate warning or error shows that the browser rejected the compromised chain.

A page that loads successfully shows that the browser accepted the chain. That outcome reveals a stale trust decision.

  • Return to the incident-provided list of eMudhra test hosts from earlier.
  • Copy the full URL for one compromised-intermediate test host.
Run the browser trust test

The browser now makes its own trust decision using its current certificate data. Before you load the host, do you expect the browser to reject the chain or accept it?

  • Switch back to your browser from earlier.
  • Click the address bar at the top of the browser.
  • Paste the test-host URL you copied.
  • Press Enter to load the host.

Your browser now shows one of two meaningful outcomes. A certificate warning or error means it rejected the compromised chain.

A successfully loaded page means the browser accepted the compromised chain.

Can't reach the test host?

A general network failure does not prove whether your browser trusts the compromised chain. Treat that result as inconclusive.

  • Return to the incident-provided test-host list.
  • Copy a different test-host URL.
  • Repeat the browser test with the new URL.
  • Help me classify the browser result.
Record the browser result

The browser result becomes useful evidence when you preserve the screen you actually saw. Keep that original outcome intact before you classify it.

  • Leave the certificate warning in place if one is displayed.
  • Classify your result as certificate warning/error or successfully loaded page.

Your first trust baseline is captured. Next, you will compare it with the terminal's curl result.

Check Trust with curl

Your browser has already shown how it handles one compromised certificate chain. That result only covers the browser.

The curl command can rely on a different trust store. This step tests two incident-provided hosts with normal certificate verification. You will record what each result means.

In this step, get ready to:
  • Run curl against the first incident-provided test host.
  • Check a second incident-provided test host.
  • Interpret each result as a rejected chain or a trusted chain.
Predict

A TLS verification error is the safe result for these test hosts. It means curl rejected the compromised chain.

A successful HTTP response means the local curl trust store accepted that chain. The security pass condition is therefore the error.

Before you test, do you expect the terminal result to match your browser result?

  • Switch back to the terminal from earlier.
  • Check the first host by running this command:
curl https://dv-g3b-emudhra.we-have-a-problem.com/

How Does This Check Work?

  • The curl command opens an HTTPS connection with normal certificate verification enabled.
  • The test host presents a chain containing a compromised intermediate certificate.
  • The resulting error or response reveals whether this curl trust store rejects that chain.
  • Classify the first result as a TLS verification error or a successful HTTP response.

You have your first terminal trust result. You now know how this curl installation treats one compromised chain.

Is the Result Inconclusive?

  • Treat a timeout or name-resolution failure as inconclusive. Re-run the command after confirming your internet connection works.
  • Confirm that the test host in your terminal exactly matches the host in the command above.
  • Help me classify my curl result.
Cross-Check

The first command covers one incident-provided host. A second host checks another compromised chain path.

Before you run the second check, do you expect curl to treat both hosts the same way?

  • Test the second host by running this command:
curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/

What Does the Second Check Add?

  • The second host presents another incident-provided compromised chain.
  • The command keeps normal TLS certificate verification enabled.
  • Using the same client settings makes the two trust results comparable.
  • Classify the second result as a TLS verification error or a successful HTTP response.
  • Compare the second classification with your first classification.

Two TLS verification errors show that curl rejects both tested chains. Any successful response shows that curl still trusts at least one compromised chain.

That is the terminal check complete. You have turned two command results into a clear decision about the local curl trust store.

Still Unable to Classify a Result?

  • Look for certificate verification language in the terminal output. A general connection failure does not prove that the chain was rejected.
  • Remove any command option that bypasses certificate verification before repeating the test.
  • Ask for help interpreting both outputs.

Your curl trust results are recorded. Next up, you will run the same kind of certificate check through Python and see whether its trust store reaches the same decision.

Update Python Certificate Trust

Your browser has tested one trust path. curl has tested another. Python can reach HTTPS services through a separate package-managed path, so the curl result cannot confirm whether Python is safe.

Requests verifies TLS connections with certificates from certifi. Certifi supplies a CA bundle that can differ from the one curl used.

You will capture Python's current result first. Then you will update the active environment. Finally, you will repeat the same request.

In this step, get ready to:
  • Test how Python handles an incident-provided certificate chain.
  • Update the active Python package tooling plus the certifi CA bundle.
  • Run the same Python trust test after the update.
Test Python before the update

Requests enables certificate verification by default. A successful response means the active Python environment accepted the compromised chain.

  • Switch back to the terminal you used for the curl checks.
  • Store one incident-provided test-host URL here: your incident-provided test-host URL.

Before you run the request, do you think Python will return a status code or stop during certificate verification?

  • Test the active Python environment by running this command:
python -c "import certifi, requests; print(certifi.__version__); print(requests.get('[[PYTHON_TEST_URL="your incident-provided test-host URL"]]').status_code)"

How Do You Read This Result?

  • A certifi version followed by an HTTP status code means Python accepted the chain. The active bundle still trusts that compromised path.
  • A certificate verification exception means Python stopped the TLS handshake. This is the safe result.
  • A network or DNS failure is inconclusive. It does not prove whether the certificate chain was trusted.

Once you have a status code or certificate verification failure, your Python baseline is locked in. That gives the update a concrete result to compare.

Can't Complete the Python Request?

  • Install any missing requests or certifi package with the package manager for this active environment.
  • Replace the test URL with another incident-provided host if the current host has gone offline.
  • help me diagnose my Python certificate trust test.
Update the package tooling and CA bundle

The package manager also handles HTTPS connections during installs. Updating that tool closes its incident-specific trust gap while the certifi update protects Requests.

  • Select the tab for the package manager that controls your active Python environment.

pip

The patched pip release for this incident is 26.2.2. The patched certifi bundle used in this project is 2026.10.19.

  • Update pip plus certifi in the active environment by running this command:
python -m pip install --upgrade "pip==26.2.2" "certifi==2026.10.19"

What Does This Update Change?

  • The pip==26.2.2 package updates pip's incident-affected trust behavior.
  • The certifi==2026.10.19 package replaces the CA bundle that Requests uses by default.
  • Verify both installed versions by running these commands:
python -m pip --version
python -c "import certifi; print(certifi.__version__)"

What Should I See?

The first line should show pip 26.2.2. The second line should show certifi 2026.10.19.

That closes the pip trust-store gap in this environment.

Versions Still Look Old?

  • Use the uv path when Python is below 3.10 because pip 26.2.2 requires Python 3.10 or newer.
  • Repeat the version check after confirming the terminal points to the same Python environment used for the install.
  • help me update pip and certifi in the correct Python environment.

uv

The patched uv release for this incident is 0.13.4 or newer. The standalone updater may refresh entries in your shell profile.

  • Update uv plus certifi in the active environment by running these commands:
uv self update
uv pip install --upgrade "certifi==2026.10.19"

What Does This Update Change?

  • The first command updates a standalone uv installation to the latest available release.
  • The second command installs certifi 2026.10.19 into the active Python environment.
  • Verify both installed versions by running these commands:
uv --version
python -c "import certifi; print(certifi.__version__)"

What Should I See?

The first line should show uv 0.13.4 or newer. The second line should show certifi 2026.10.19.

Your uv-managed environment now carries the patched trust data.

Self-Update Unavailable?

  • Use the installer that originally installed uv if self-update is disabled. Update to 0.13.4 or newer.
  • Select the intended Python environment if certifi remains on an older version.
  • help me update uv and certifi in the correct environment.
Repeat the Python trust test

A fair before-and-after comparison uses the same client. It also uses the same incident host.

Before you rerun the request, do you think Python will return a status code or stop during certificate verification now?

  • Retest the updated Python environment by running the same command:
python -c "import certifi, requests; print(certifi.__version__); print(requests.get('[[PYTHON_TEST_URL="your incident-provided test-host URL"]]').status_code)"

What Changed?

  • The host remains the same as your baseline test.
  • The Python client remains Requests.
  • The active certifi bundle is now the patched 2026.10.19 release.

You should now see certifi 2026.10.19 followed by a certificate verification failure. Python should stop before printing an HTTP status code.

That is the safety signal you wanted: Python refuses the compromised chain. If your baseline already failed this way, the repeated failure confirms that the active environment remains protected.

Still Getting an HTTP Status?

  • Repeat the package version check to confirm that this terminal is running the updated environment.
  • Inspect any intentional REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE override because Requests can use those paths instead of its default bundle.
  • help me find why Requests still trusts the compromised test chain.

Your Python environment now has patched package tooling plus a recorded trust retest. Next, you will turn these one-off checks into a script that fails whenever a compromised chain is accepted.

Create the Trust Check Script

Your updated Python environment has already tested the compromised chain. The remaining risk is a manual result that can be forgotten or misread.

An automated trust check turns that result into a repeatable CI signal. Its pass condition is inverted because a TLS certificate-validation failure proves the compromised chain was rejected.

In this step, get ready to:
  • Create a Python script with one configurable test-host list.
  • Classify certificate-validation failures as safe results.
  • Observe the script's local exit result.
Create the URL check

These public test hosts are tied to one incident. A single list lets you replace them without changing the checking logic.

The Requests library verifies TLS certificates by default. The script keeps that verification explicit so a successful connection reveals a trust failure.

  • Use your code editor's file sidebar to create check_compromised_ca.py inside the repository from earlier.
  • Use the first incident-provided test-host URL for the first code placeholder.
  • Use the second incident-provided test-host URL for the second code placeholder.
  • Add the URL checker by pasting this code:
import sys

import requests
from requests.exceptions import RequestException, SSLError

# Keep the incident hosts together so they are easy to replace.
TEST_URLS = [
    "[[TEST_HOST_URL_1=\"first incident-provided test-host URL\"]]",
    "[[TEST_HOST_URL_2=\"second incident-provided test-host URL\"]]",
]

# Classify the trust result for one configured host.
def check_url(url):
    try:
        response = requests.get(url, timeout=10, verify=True)
    except SSLError:
        print(f"SAFE: {url} was rejected during certificate validation.")
        return "safe"
    except RequestException as error:
        print(f"CHECK ERROR: {url} could not be tested: {error}")
        return "error"

    print(f"TRUST FAILURE: {url} returned HTTP {response.status_code}.")
    return "failure"

What does this code do?

  • The TEST_URLS list keeps every incident test host in one configurable location.
  • The verify=True argument keeps certificate verification active for every request.
  • An SSLError produces a safe result because the TLS connection was rejected.
  • A successful HTTP response produces a trust failure because the compromised chain was accepted.
  • A RequestException records a connectivity problem as an inconclusive check.
  • Save check_compromised_ca.py.
  • Confirm that check_compromised_ca.py is listed in your code editor's file sidebar.

Good progress. Your incident test hosts now live in one place that you can update later.

Does the file look incomplete?

  • Check that both code placeholders contain the complete test-host URLs from earlier.
  • Match the indentation inside check_url() to the code block.
  • Check that every URL remains inside the TEST_URLS list.
  • help me compare my URL checker with the expected Python structure.
Finish and run the script

A process exit status gives terminals and CI systems a machine-readable result. Exit status 0 means every configured chain was rejected.

Exit status 1 means at least one host was trusted or could not be checked. This fail-closed behavior prevents an inconclusive run from looking safe.

  • Add the final decision logic below check_url() by pasting this code:
# Check every host before calculating one process exit status.
results = [check_url(url) for url in TEST_URLS]
has_problem = any(result != "safe" for result in results)
exit_code = 1 if has_problem else 0

# Print a readable summary before returning the machine-readable result.
if exit_code:
    print("FAIL: At least one host was trusted or could not be checked.")
else:
    print("PASS: Every configured host was rejected during certificate validation.")

print(f"Exit code: {exit_code}")
sys.exit(exit_code)

How does the exit decision work?

  • The list comprehension runs check_url() once for every configured host.
  • The has_problem value becomes true when any result is not safe.
  • The exit_code value converts the combined result into a status that automation can read.
  • The final sys.exit() call returns that status to the terminal.
  • Save check_compromised_ca.py.
  • Move the terminal from earlier into your repository's top-level folder.

Before you run the script, do you expect the updated Python environment to return exit status 0 or 1?

  • Run the local trust check with this command:
python check_compromised_ca.py

How should I read the result?

When every configured host is rejected, you see one SAFE line per URL. The final lines show PASS plus Exit code: 0.

A successful request produces TRUST FAILURE plus Exit code: 1. Your active Python environment still accepts that compromised chain.

A connectivity problem produces CHECK ERROR plus Exit code: 1. Replace an unavailable incident URL before treating the check as conclusive.

That is your local guardrail working. A safe certificate rejection now becomes an exit result that automation can enforce.

Did the script stop before the summary?

  • Activate the same Python environment that contains the updated certifi bundle if you see a dependency import problem.
  • Compare both URL values with the incident-provided hosts if you see CHECK ERROR.
  • Restore the missing indentation if Python points to a line inside check_url().
  • help me diagnose why my local trust-check script does not reach its final exit summary.

✔️ Awesome, I've got everything!

Your script now checks every configured test host. It prints a readable summary before returning a machine-readable exit status.

ⓧ I'd like to double check the full code

Compare your saved check_compromised_ca.py file with this complete version.

import sys

import requests
from requests.exceptions import RequestException, SSLError

# Keep the incident hosts together so they are easy to replace.
TEST_URLS = [
    "[[TEST_HOST_URL_1=\"first incident-provided test-host URL\"]]",
    "[[TEST_HOST_URL_2=\"second incident-provided test-host URL\"]]",
]

# Classify the trust result for one configured host.
def check_url(url):
    try:
        response = requests.get(url, timeout=10, verify=True)
    except SSLError:
        print(f"SAFE: {url} was rejected during certificate validation.")
        return "safe"
    except RequestException as error:
        print(f"CHECK ERROR: {url} could not be tested: {error}")
        return "error"

    print(f"TRUST FAILURE: {url} returned HTTP {response.status_code}.")
    return "failure"

# Check every host before calculating one process exit status.
results = [check_url(url) for url in TEST_URLS]
has_problem = any(result != "safe" for result in results)
exit_code = 1 if has_problem else 0

# Print a readable summary before returning the machine-readable result.
if exit_code:
    print("FAIL: At least one host was trusted or could not be checked.")
else:
    print("PASS: Every configured host was rejected during certificate validation.")

print(f"Exit code: {exit_code}")
sys.exit(exit_code)

Your local script now turns compromised-chain trust into a result that other tools can enforce. Next, you will run the same guardrail with GitHub Actions whenever your repository checks this risk.

Run the Check in GitHub Actions

Your local script shows how one machine handles the compromised certificate chains. A future trust-store change could silently reverse that result.

In this step, you will use GitHub Actions to run the same script after every push. The runner installs the current certifi CA bundle before checking the configured hosts.

In this step, get ready to:
  • Create a push-triggered workflow for the trust-check script.
  • Push the workflow file to GitHub.
  • Review the workflow result from a fresh runner.
Create the workflow file

A GitHub Actions workflow is a YAML file inside .github/workflows. GitHub reads files from this location when repository events occur.

  • Return to the code editor for the repository containing check_compromised_ca.py.
  • Use the editor's new-file control to create .github/workflows/compromised-ca-check.yml.

You should see compromised-ca-check.yml nested inside .github/workflows in the editor's file list.

  • Populate .github/workflows/compromised-ca-check.yml by pasting this workflow:
name: Compromised CA trust check
# Run the trust check whenever repository changes are pushed
on:
  push:

# Limit the workflow to reading repository contents
permissions:
  contents: read

# Test the configured hosts from a fresh hosted runner
jobs:
  check-compromised-ca:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v6
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      # Install the HTTP dependency and current certificate bundle
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install --upgrade requests certifi

      # Preserve the script exit code as the job result
      - name: Run compromised CA check
        run: python check_compromised_ca.py

What does this workflow do?

  • The push trigger starts the workflow when a commit reaches GitHub.
  • The contents: read permission gives the job read-only access to the repository.
  • actions/checkout@v6 places check_compromised_ca.py on the runner.
  • actions/setup-python@v5 prepares a Python 3.x runtime.
  • pip installs current copies of requests plus certifi.
  • The final step runs the existing script. A non-zero script exit makes the job fail.
  • Save .github/workflows/compromised-ca-check.yml.
  • Confirm that the saved file still appears inside .github/workflows.

Your repository now contains the automation that repeats your local trust check on a hosted runner.

Workflow file not appearing?

  • Check that the filename ends with .yml.
  • Confirm that the workflows folder sits inside .github.
  • Compare the indentation under jobs with the workflow above.
  • Ask for help if the workflow structure still looks wrong: help me check my GitHub Actions workflow structure.

✔️ Awesome, I've got everything!

Good work. Your saved workflow checks out the repository before preparing Python and running the trust check.

ⓧ I'd like to double check the full code

Compare the complete contents of .github/workflows/compromised-ca-check.yml with this reference.

name: Compromised CA trust check
# Run the trust check whenever repository changes are pushed
on:
  push:

# Limit the workflow to reading repository contents
permissions:
  contents: read

# Test the configured hosts from a fresh hosted runner
jobs:
  check-compromised-ca:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v6
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      # Install the HTTP dependency and current certificate bundle
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install --upgrade requests certifi

      # Preserve the script exit code as the job result
      - name: Run compromised CA check
        run: python check_compromised_ca.py
Push the workflow

Git records the workflow file in a commit. Pushing that commit creates the event that starts the new workflow.

An authentication prompt may appear during the push. It authorizes this repository update without exposing your credentials in the command.

  • Return to the terminal for the repository containing check_compromised_ca.py.
  • Stage the workflow file before committing it by running these commands:
git add .github/workflows/compromised-ca-check.yml
git commit -m "Add compromised CA trust check"
git push --set-upstream origin HEAD

What do these commands do?

  • The first command stages only .github/workflows/compromised-ca-check.yml.
  • The second command records the staged workflow in the local repository history.
  • The third command sends the current branch to the origin remote.
  • The completed push triggers the workflow's push event.

You should see Git report that the commit reached the remote branch. GitHub now has the workflow plus the script it runs.

Push did not complete?

  • Confirm that the terminal is inside the repository containing check_compromised_ca.py.
  • Authenticate with a GitHub account that can push to the repository.
  • Review the remote changes if Git reports that the branch is behind its remote counterpart.
  • Ask for help without sharing credentials: help me troubleshoot this Git push.
Review the workflow run

The hosted runner provides an independent TLS trust result. The job succeeds only when every configured request ends in a certificate-validation failure.

Before you check the newest run, do you expect the runner to accept or reject the compromised chains?

  • Return to the repository's main page on GitHub.
  • Click Actions under the repository name.
  • Select Compromised CA trust check in the left sidebar.
  • Click the newest run to open its workflow run summary.

The runner can take a short while to start because GitHub prepares a fresh environment. The dependency step also downloads current Python packages.

  • Click the check-compromised-ca job in the run summary.
  • Expand Run compromised CA check to inspect the result for each configured host.

You will see the run finish with a success or failure result. The script output shows how the runner handled every configured test host.

How should I read the result?

A successful job means every configured request reached a certificate-validation failure. The runner rejected each compromised chain.

A failure in Run compromised CA check means at least one request succeeded. The runner trusted a compromised chain for that host.

Workflow run failed unexpectedly?

  • Check which workflow step failed before interpreting the result.
  • Review the dependency step if the runner could not install requests or certifi.
  • Review the script output if Run compromised CA check failed.
  • Ask for help interpreting the run: help me diagnose this compromised CA workflow failure.

Strong finish. Every push now tests a fresh runner against the compromised certificate chains.

Secret mission

Schedule the Compromised CA Check

A passing workflow captures the runner's trust decision today. Add a weekly schedule so a future runner with a stale trust store exposes the regression automatically.

Clean Up Your Resources

Clean Up Your Resources

Your trust checks create no paid cloud resources. Choose whether to keep the scheduled GitHub Actions check active, pause it for later, or delete the project files.

Resources you used:

  • The local check_compromised_ca.py trust-check script.
  • The check_compromised_ca.py script in your GitHub repository.
  • The local .github/workflows/compromised-ca-check.yml workflow file.
  • The scheduled workflow stored at .github/workflows/compromised-ca-check.yml in your GitHub repository.

Keep everything running

Everything can stay in place if you want the trust check to keep guarding your repository.

  • Keep both local project files in your repository.
  • Keep both project files on GitHub.
  • Leave the scheduled workflow enabled so recurring checks continue.
  • Update the centralized test-host list in check_compromised_ca.py when an incident host goes offline.

Pause - I'll come back to this later

Disable the workflow when you want to stop future runs without losing the script or its configuration.

  • Return to the GitHub repository from earlier.
  • Return to its GitHub Actions page.
  • Select the workflow associated with .github/workflows/compromised-ca-check.yml.
  • Select the disable option from the workflow controls.
  • Confirm that the workflow page identifies the workflow as disabled.
  • Leave check_compromised_ca.py in your local repository.
  • Leave .github/workflows/compromised-ca-check.yml in your local repository.

Your recurring check is now paused. Both files remain ready for reactivation.

Delete - I don't want to use this again

Delete the project files from GitHub to stop the scheduled check. Remove their local copies to finish the cleanup.

  • Return to the file view for your GitHub repository from earlier.
  • Open .github/workflows/compromised-ca-check.yml from the repository file list.
  • Select the delete option from the file controls.
  • Commit the deletion to the repository's default branch.
  • Open check_compromised_ca.py from the top level of the repository.
  • Select the delete option from the file controls.
  • Commit the deletion to the repository's default branch.

The remote workflow definition is gone, so its schedule cannot start new runs.

  • Delete check_compromised_ca.py from the top level of your local repository using its file browser.
  • Delete .github/workflows/compromised-ca-check.yml from your local repository using the same file browser.
  • Confirm that both files are absent from your local repository.
  • Return to the default branch on GitHub to confirm that both paths are absent.

Cleanup complete. Your patched trust stores remain in place while every project-specific artifact is removed.

Nice Work!

Nice Work!

Congratulations on building a repeatable TLS trust check for the compromised eMudhra certificate chains. Your browser, terminal, Python environment, and GitHub Actions workflow now expose stale trust before it reaches another run.

You've learned how to:

  • Tested three different trust paths through your browser, curl, and Python. Interpreted a certificate-validation error as proof that the compromised chain was rejected.
  • Updated Python's package tooling and the certifi CA bundle. Confirmed that the post-update request rejects the compromised chain.
  • Connected check_compromised_ca.py to a GitHub Actions workflow that fails whenever any configured test host is trusted.
  • Secret Mission: Added a recurring schedule plus a single URL configuration point so the check stays useful when incident hosts change.

Ready to quiz yourself?