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.
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
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
-
Prediction A:
http://127.0.0.1:8794will pass preflight, buthttps://production.example.com, an unrelated test-looking hostname, and the wrong local port will all be rejected before any session ID exists. -
Prediction B: the deliberately broken
bad.logwill fail the secret audit; after redaction the exact fake secret will no longer be found. - 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?
Safety policy should not depend on browser availability. Independent preflight proves forbidden targets are contained before session/network/application mutation.
What should happen if the functional Selenium test passes but the artifact audit finds a secret?
The pipeline should fail the safety gate. Functional success does not override a security/privacy violation.
A future chapter needs a real admin-only action. What should you do first?
Question whether a browser test is necessary; if it is, use a specially approved isolated test tenant/identity and explicit authorization rather than broadening routine suite privilege.
Why should Grid evidence appear in the packet only when RemoteWebDriver is used?
Evidence should match actual architecture. Local browser runs have no Grid routing layer; inventing Grid evidence would reduce diagnostic clarity.
What production operating-model capability does Chapter 24 add?
A fail-closed governance layer around target authorization, identity/data isolation, secrets, browser/network boundaries, evidence minimization, retention, and cleanup.
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.
Primary references and version notes
- Selenium downloads — stable client and Grid release baseline.
- Selenium documentation — WebDriver, Selenium Manager, Grid, and supported project components.
- Getting started with Selenium Grid — current Grid security warning and capacity guidance.
- Overview of test automation — keep browser tests focused and deliberate.
- Selenium Python 4.47 API — supported Python/browser baseline.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.