Chapter 24Lesson 05~210 minutes

Checkpoint Lab — Security, Privacy, Test Accounts, and Safe Automation Boundaries

This checkpoint turns the chapter into an operating control: threat-model the pipeline, prove forbidden targets are blocked, audit evidence, and leave behind a reusable safe-automation checklist.

CheckpointThreat modelPolicy gateRunbook

Learning objectives

  • Threat-model assets, actors, trust boundaries, and failure consequences for a Selenium pipeline.
  • Implement and independently verify target allowlisting before WebDriver creation.
  • Inject only fake lab secrets and prove evidence contains no exact secret values.
  • Simulate and diagnose a blocked production-like target and a secret-leak artifact.
  • Produce a checklist that can gate later performance/framework/capstone work.

1. Scenario and preflight

You own a six-step browser pipeline: CI selects environment → safety preflight → browser session → synthetic login → assertion/evidence → cleanup. The checkpoint must prove that a bad environment selection never reaches WebDriver and that a diagnostic artifact containing a fake secret is detected.

Preflight item Required state Evidence
Selenium binding Python 4.47.0; Python 3.10+ version output / requirements pin
browser/driver supported local browser; Selenium Manager normal resolution returned capabilities if live browser path is run
target 127.0.0.1:8794 only allowlist configuration
identity synthetic viewer account only account class, not secret value
Grid not required; if used, private and authorized Grid endpoint/network policy evidence
artifacts temporary/restricted lab directory artifact inventory + audit result
production explicitly forbidden blocked-target test before browser creation

2. Threat model before execution

Checkpoint trust boundaries

The following diagram visualizes the relationships described in Threat model before execution. Read the nodes in sequence and use the arrows to connect the conceptual state changes to the explanation around the diagram.

flowchart TD
C[CI / test runner] -->|config + secret refs| P[Safety preflight]
P -->|authorized target only| W[WebDriver / private Grid]
W --> B[Disposable browser profile]
B --> A[Loopback test AUT]
A --> E[Minimal evidence]
E --> Q[Redaction / audit]
Q --> R[Approved retention or cleanup]

Threats to name before the run: malicious/accidental base URL change, over-privileged account, secret printed by debug code, personal browser profile reuse, PII in screenshot/download, public Grid ingress, stale artifact retention, and test code attempting to bypass an identity/anti-abuse control.

3. Generate the lab and set synthetic secrets

The following example makes the Generate the lab and set synthetic secrets behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from pathlib import Path
root = Path('selenium-safe-lab')
root.mkdir(exist_ok=True)
(root/'safety_harness.py').write_text('from __future__ import annotations\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom urllib.parse import urlparse\nimport hashlib, json, os, re, shutil, tempfile\n\nclass UnsafeTargetError(RuntimeError):\n    pass\n\n@dataclass(frozen=True)\nclass SafetyConfig:\n    base_url: str\n    allowed_hosts: frozenset[str]\n    allowed_ports: frozenset[int]\n    artifact_root: Path\n\nPRODUCTION_MARKERS = {"prod", "production", "live", "customer", "payments"}\n\n\ndef assert_authorized_target(url: str, cfg: SafetyConfig) -> None:\n    p = urlparse(url)\n    host = (p.hostname or "").lower()\n    port = p.port or (443 if p.scheme == "https" else 80)\n    labels = set(host.replace("-", ".").split("."))\n    if p.scheme not in {"http", "https"}:\n        raise UnsafeTargetError("unsupported scheme")\n    if host not in cfg.allowed_hosts or port not in cfg.allowed_ports:\n        raise UnsafeTargetError("target is outside the explicit test allowlist")\n    if labels & PRODUCTION_MARKERS:\n        raise UnsafeTargetError("production-like target marker refused")\n\n\ndef require_fake_secret(name: str) -> str:\n    value = os.environ.get(name, "")\n    if not value:\n        raise RuntimeError(f"missing required environment variable: {name}")\n    if not value.startswith("LAB_ONLY_"):\n        raise RuntimeError(f"{name} must use the lab-only synthetic prefix")\n    return value\n\n\ndef redact_text(text: str, secrets: list[str]) -> str:\n    clean = text\n    for secret in secrets:\n        if secret:\n            clean = clean.replace(secret, "[REDACTED]")\n    clean = re.sub(r\'(?i)(password|token|secret)(["\\\\s:=]+)[^\\\\s,;]+\', r\'\\\\1\\\\2[REDACTED]\', clean)\n    return clean\n\n\ndef secret_fingerprint(value: str) -> str:\n    return hashlib.sha256(value.encode()).hexdigest()[:12]\n\n\ndef audit_tree(root: Path, forbidden: list[str]) -> list[str]:\n    findings = []\n    for path in root.rglob("*"):\n        if not path.is_file():\n            continue\n        if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".zip"}:\n            continue\n        text = path.read_text(encoding="utf-8", errors="ignore")\n        for value in forbidden:\n            if value and value in text:\n                findings.append(f"{path.name}: exact synthetic secret present")\n    return findings\n\n\ndef disposable_workspace(prefix="selenium-safe-"):\n    root = Path(tempfile.mkdtemp(prefix=prefix))\n    (root / "profile").mkdir()\n    (root / "downloads").mkdir()\n    (root / "artifacts").mkdir()\n    return root\n\n\ndef cleanup_workspace(root: Path) -> None:\n    shutil.rmtree(root, ignore_errors=True)\n', encoding='utf-8')
(root/'aut_server.py').write_text('from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler\nfrom urllib.parse import parse_qs\nfrom http.cookies import SimpleCookie\nimport secrets\n\nHOST, PORT = "127.0.0.1", 8794\nUSER = "LAB_ONLY_user_24"\nPASSWORD = "LAB_ONLY_password_24"\nSESSIONS = set()\n\nPAGE = b"""<!doctype html><html><head><title>Safe Automation Lab</title></head><body>\n<h1>Safe Automation Lab</h1><p id=\'environment\'>environment=test</p>\n<form method=\'post\' action=\'/login\'>\n<label>User <input id=\'username\' name=\'username\' data-sensitive=\'true\'></label>\n<label>Password <input id=\'password\' name=\'password\' type=\'password\' data-sensitive=\'true\'></label>\n<button id=\'sign-in\' type=\'submit\'>Sign in</button></form>\n<p id=\'notice\'>Synthetic credentials only.</p></body></html>"""\n\n\ndef cookie_value(raw):\n    jar = SimpleCookie(); jar.load(raw or "")\n    m = jar.get("lab_session")\n    return m.value if m else None\n\nclass Handler(BaseHTTPRequestHandler):\n    def send_body(self, status, body, headers=()):\n        self.send_response(status)\n        self.send_header("Content-Type", "text/html; charset=utf-8")\n        self.send_header("Cache-Control", "no-store")\n        for k, v in headers: self.send_header(k, v)\n        self.end_headers(); self.wfile.write(body)\n    def do_GET(self):\n        if self.path == "/health": return self.send_body(200, b"ok")\n        if self.path == "/login": return self.send_body(200, PAGE)\n        if self.path == "/dashboard":\n            token = cookie_value(self.headers.get("Cookie"))\n            if token not in SESSIONS:\n                return self.send_body(401, b"<h1 id=\'state\'>not-authenticated</h1>")\n            body = f"<h1 id=\'state\'>authenticated</h1><p id=\'user\'>{USER}</p><p id=\'scope\'>role=viewer</p>".encode()\n            return self.send_body(200, body)\n        return self.send_body(404, b"not found")\n    def do_POST(self):\n        if self.path != "/login": return self.send_body(404, b"not found")\n        n = int(self.headers.get("Content-Length", "0"))\n        form = parse_qs(self.rfile.read(n).decode())\n        if form.get("username", [""])[0] != USER or form.get("password", [""])[0] != PASSWORD:\n            return self.send_body(401, b"<h1 id=\'state\'>invalid-credentials</h1>")\n        token = secrets.token_urlsafe(24); SESSIONS.add(token)\n        self.send_response(303)\n        self.send_header("Location", "/dashboard")\n        self.send_header("Set-Cookie", f"lab_session={token}; Path=/; HttpOnly; SameSite=Lax")\n        self.send_header("Cache-Control", "no-store")\n        self.end_headers()\n    def log_message(self, fmt, *args):\n        # Deliberately avoid request bodies, Cookie, Authorization, or credential values.\n        print("AUT", self.command, self.path.split("?", 1)[0])\n\nThreadingHTTPServer((HOST, PORT), Handler).serve_forever()\n', encoding='utf-8')
(root/'safe_test.py').write_text('import json, os\nfrom pathlib import Path\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom safety_harness import (SafetyConfig, assert_authorized_target, require_fake_secret,\n                            redact_text, secret_fingerprint, audit_tree,\n                            disposable_workspace, cleanup_workspace)\n\nBASE_URL = os.environ.get("SEL_SAFE_BASE_URL", "http://127.0.0.1:8794")\nUSER = require_fake_secret("SEL_SAFE_USER")\nPASSWORD = require_fake_secret("SEL_SAFE_PASSWORD")\nroot = disposable_workspace()\nartifacts = root / "artifacts"\n\ncfg = SafetyConfig(\n    base_url=BASE_URL,\n    allowed_hosts=frozenset({"127.0.0.1", "localhost"}),\n    allowed_ports=frozenset({8794}),\n    artifact_root=artifacts,\n)\nassert_authorized_target(BASE_URL, cfg)  # must run before browser mutation\n\noptions = webdriver.ChromeOptions()\noptions.add_argument("--headless=new")\noptions.add_argument(f"--user-data-dir={root / \'profile\'}")\noptions.add_experimental_option("prefs", {"download.default_directory": str(root / "downloads")})\n\ndriver = None\ntry:\n    driver = webdriver.Chrome(options=options)\n    caps = dict(driver.capabilities)\n    driver.get(BASE_URL + "/login")\n    assert driver.find_element(By.ID, "environment").text == "environment=test"\n    driver.find_element(By.ID, "username").send_keys(USER)\n    driver.find_element(By.ID, "password").send_keys(PASSWORD)\n    driver.find_element(By.ID, "sign-in").click()\n    WebDriverWait(driver, 5).until(EC.text_to_be_present_in_element((By.ID, "state"), "authenticated"))\n    assert driver.find_element(By.ID, "scope").text == "role=viewer"\n\n    # Screenshot only after credential fields are gone from the authenticated page.\n    driver.save_screenshot(str(artifacts / "authenticated.png"))\n    cookie_meta = [{"name": c["name"], "httpOnly": c.get("httpOnly"), "sameSite": c.get("sameSite")} for c in driver.get_cookies()]\n    manifest = {\n        "session_id": driver.session_id,\n        "browser": caps.get("browserName"),\n        "browser_version": caps.get("browserVersion"),\n        "base_url": BASE_URL,\n        "url": driver.current_url,\n        "title": driver.title,\n        "account_scope": "viewer",\n        "password_fingerprint": secret_fingerprint(PASSWORD),\n        "cookies": cookie_meta,\n    }\n    (artifacts / "manifest.json").write_text(redact_text(json.dumps(manifest, indent=2), [USER, PASSWORD]))\n    findings = audit_tree(artifacts, [USER, PASSWORD])\n    if findings:\n        raise AssertionError("artifact audit failed: " + "; ".join(findings))\nfinally:\n    if driver is not None:\n        driver.quit()\n    # A real CI job would upload reviewed artifacts before this point, then invoke cleanup.\n    cleanup_workspace(root)\n', encoding='utf-8')
(root/'checkpoint.py').write_text('import json, os, tempfile\nfrom pathlib import Path\nfrom safety_harness import SafetyConfig, UnsafeTargetError, assert_authorized_target, require_fake_secret, redact_text, audit_tree\n\nallowed = SafetyConfig(\n    base_url="http://127.0.0.1:8794",\n    allowed_hosts=frozenset({"127.0.0.1", "localhost"}),\n    allowed_ports=frozenset({8794}),\n    artifact_root=Path("evidence"),\n)\n\n# Prediction 1: this target is authorized and passes before any browser starts.\nassert_authorized_target(allowed.base_url, allowed)\n\n# Prediction 2: a production-like or merely non-allowlisted target is refused before session creation.\nblocked = []\nfor target in ["https://production.example.com", "https://shop.example.com", "http://127.0.0.1:9999"]:\n    try:\n        assert_authorized_target(target, allowed)\n    except UnsafeTargetError as exc:\n        blocked.append({"target": target, "classification": type(exc).__name__})\n\nif len(blocked) != 3:\n    raise AssertionError("the safety boundary did not block every forbidden target")\n\npassword = require_fake_secret("SEL_SAFE_PASSWORD")\nwith tempfile.TemporaryDirectory(prefix="safe-evidence-") as tmp:\n    root = Path(tmp)\n    # Deliberately broken artifact: represents a bad logger that persisted a secret.\n    bad = root / "bad.log"\n    bad.write_text(f"login password={password}\\\\n")\n    first_findings = audit_tree(root, [password])\n    if not first_findings:\n        raise AssertionError("expected secret-leak audit finding")\n\n    # Least destructive repair: preserve the finding, redact the artifact, then re-audit.\n    bad.write_text(redact_text(bad.read_text(), [password]))\n    second_findings = audit_tree(root, [password])\n    if second_findings:\n        raise AssertionError("redaction repair failed")\n\n    packet = {\n        "blocked_targets": blocked,\n        "first_audit_findings": first_findings,\n        "post_redaction_findings": second_findings,\n        "safe_automation_checklist": [\n            "written target authorization / allowlist",\n            "least-privilege synthetic test identity",\n            "no MFA or anti-abuse bypass",\n            "disposable browser profile and downloads",\n            "redacted/minimized evidence",\n            "private Grid/network boundary",\n            "retention owner and expiration",\n            "failure-safe cleanup",\n        ],\n    }\n    (root / "checkpoint.json").write_text(json.dumps(packet, indent=2))\n    print((root / "checkpoint.json").read_text())\n', encoding='utf-8')
(root/'requirements.txt').write_text('selenium==4.47.0\n', encoding='utf-8')
print(root.resolve())

The following example makes the Generate the lab and set synthetic secrets behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

cd selenium-safe-lab
python -m venv .venv
# Activate the venv, then:
python -m pip install -r requirements.txt

# PowerShell
$env:SEL_SAFE_PASSWORD = "LAB_ONLY_password_24"
$env:SEL_SAFE_USER = "LAB_ONLY_user_24"
# POSIX
# export SEL_SAFE_PASSWORD='LAB_ONLY_password_24'
# export SEL_SAFE_USER='LAB_ONLY_user_24'

4. Predict before acting

  1. Prediction A: http://127.0.0.1:8794 will pass preflight, but https://production.example.com, an unrelated test-looking hostname, and the wrong local port will all be rejected before any session ID exists.
  2. Prediction B: the deliberately broken bad.log will fail the secret audit; after redaction the exact fake secret will no longer be found.
  3. Prediction C (optional live browser path): the authenticated page will show viewer scope, and retained cookie evidence will contain metadata rather than the cookie value.

5. Run the deterministic safety checkpoint

The following example makes the Run the deterministic safety checkpoint behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

import json, os, tempfile
from pathlib import Path
from safety_harness import SafetyConfig, UnsafeTargetError, assert_authorized_target, require_fake_secret, redact_text, audit_tree

allowed = SafetyConfig(
    base_url="http://127.0.0.1:8794",
    allowed_hosts=frozenset({"127.0.0.1", "localhost"}),
    allowed_ports=frozenset({8794}),
    artifact_root=Path("evidence"),
)

# Prediction 1: this target is authorized and passes before any browser starts.
assert_authorized_target(allowed.base_url, allowed)

# Prediction 2: a production-like or merely non-allowlisted target is refused before session creation.
blocked = []
for target in ["https://production.example.com", "https://shop.example.com", "http://127.0.0.1:9999"]:
    try:
        assert_authorized_target(target, allowed)
    except UnsafeTargetError as exc:
        blocked.append({"target": target, "classification": type(exc).__name__})

if len(blocked) != 3:
    raise AssertionError("the safety boundary did not block every forbidden target")

password = require_fake_secret("SEL_SAFE_PASSWORD")
with tempfile.TemporaryDirectory(prefix="safe-evidence-") as tmp:
    root = Path(tmp)
    # Deliberately broken artifact: represents a bad logger that persisted a secret.
    bad = root / "bad.log"
    bad.write_text(f"login password={password}\\n")
    first_findings = audit_tree(root, [password])
    if not first_findings:
        raise AssertionError("expected secret-leak audit finding")

    # Least destructive repair: preserve the finding, redact the artifact, then re-audit.
    bad.write_text(redact_text(bad.read_text(), [password]))
    second_findings = audit_tree(root, [password])
    if second_findings:
        raise AssertionError("redaction repair failed")

    packet = {
        "blocked_targets": blocked,
        "first_audit_findings": first_findings,
        "post_redaction_findings": second_findings,
        "safe_automation_checklist": [
            "written target authorization / allowlist",
            "least-privilege synthetic test identity",
            "no MFA or anti-abuse bypass",
            "disposable browser profile and downloads",
            "redacted/minimized evidence",
            "private Grid/network boundary",
            "retention owner and expiration",
            "failure-safe cleanup",
        ],
    }
    (root / "checkpoint.json").write_text(json.dumps(packet, indent=2))
    print((root / "checkpoint.json").read_text())

Expected output is a JSON document with three blocked targets, at least one first-audit finding, zero post-redaction findings, and the eight-item safe-automation checklist. This part does not need a browser; it tests the safety controls independently so a missing browser cannot disable policy enforcement.

6. Optional live browser verification against the local AUT

The following example makes the Optional live browser verification against the local AUT behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Terminal 1
python aut_server.py

# Terminal 2 — after setting SEL_SAFE_USER / SEL_SAFE_PASSWORD
python safe_test.py

Verify: returned session/browser evidence exists only for the authorized target; the page reports role=viewer; the browser uses a temporary profile/download directory; screenshot is taken after credential fields are gone; cookie values are not written; exact secret audit is empty; cleanup removes the temporary workspace after the reviewed evidence has been handled.

7. Evidence packet

  • Version baseline: Selenium binding and browser/driver capabilities for any live run.
  • Normalized selected base URL and allowlist decision.
  • Blocked-target classifications with no session ID for those attempts.
  • Synthetic account role/class, never reusable secret value.
  • Artifact inventory, capture policy, and exact-secret audit result.
  • Any screenshot/DOM/log evidence after privacy minimization.
  • Grid/private-network evidence if RemoteWebDriver is used.
  • Cleanup result and retention owner/expiry for anything intentionally kept.
  • A short conclusion mapping each failure to environment, identity, browser, network/Grid, AUT, test code, or evidence pipeline.

8. Safe-automation checklist for future chapters

This checklist explicitly preserves MFA, CAPTCHA, rate-limit, and anti-abuse controls. Approved test hooks are acceptable only when they are scoped to a test tenant and governed by the owning security/product team; production bypass/evasion is outside the automation boundary.

Gate Pass condition
authorization target/tenant/action explicitly approved and allowlisted before session creation
identity least-privilege synthetic/test account; no personal/shared admin identity
anti-abuse/MFA approved test path; no bypass/evasion techniques
browser state fresh disposable profile/download directory; no personal data
network/Grid private authorized endpoints; no public Grid exposure
secrets protected injection; absent from source, URL, CLI, logs, screenshots, downloads
evidence minimal, redacted, collision-safe, access-controlled
retention owner + expiry + deletion mechanism
cleanup failure-safe teardown removes disposable state
change review safety-control changes reviewed like deployment/security policy

9. Cleanup and rollback

The following example makes the Cleanup and rollback behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# Stop aut_server.py with Ctrl+C if it is running.
cd ..
rm -rf selenium-safe-lab
# PowerShell: Remove-Item -Recurse -Force selenium-safe-lab
# Clear the synthetic environment variables from the shell/session.

If a real secret or PII was accidentally captured during any non-lab run, do not simply delete local files and declare success. Follow the organization’s incident/credential-rotation/privacy process, including artifact revocation/deletion from remote CI storage and secret rotation where appropriate.

Knowledge checks

Answer from the operating model, then reveal the explanation.

Why does the deterministic checkpoint test the production guard without starting a browser?

What should happen if the functional Selenium test passes but the artifact audit finds a secret?

A future chapter needs a real admin-only action. What should you do first?

Why should Grid evidence appear in the packet only when RemoteWebDriver is used?

What production operating-model capability does Chapter 24 add?

Summary and next bridge

  • Safe automation begins with explicit authorization and least privilege, not WebDriver syntax.
  • Policy guards are independently testable and fail before browser mutation.
  • Secrets and PII are minimized across profiles, downloads, logs, screenshots, URLs, and CI artifacts.
  • Grid/network exposure, MFA/anti-abuse, and enterprise policy are security boundaries to respect, not bypass.
  • A reusable checklist now gates the remaining performance, framework, debugging, governance, and capstone chapters.

Chapter 25 moves from safety governance to performance engineering for test suites and Grid capacity. The safety gates from this chapter remain invariant while throughput is measured and optimized.

Next chapter

Performance Engineering for Test Suites and Grid Capacity: Core Concepts and Mental Model

Continue with Performance Engineering for Test Suites and Grid Capacity: Core Concepts and Mental Model. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Primary references and version notes

Version baseline — August 2026

The mandatory examples pin selenium==4.47.0 and Python 3.10+. Selenium Manager remains the normal local driver-resolution path. Browser/OS policy, identity authorization, secret storage, Grid network controls, retention, and production approvals are infrastructure/governance state and are intentionally not hidden inside Selenium helpers.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.