PRACTICAL GUIDE / Selenium internal CA certificate testing

Test internal TLS without teaching Selenium to ignore certificates

Diagnose internal HTTPS failures, provision browser trust in CI, and keep Selenium certificate tests meaningful without hiding broken TLS chains.

By The Testing AcademyUpdated August 4, 202622 min read
All field guides
In this guide6 sections
  1. Separate trusted navigation from certificate bypass
  2. Inspect the connection before changing browser options
  3. Provision a clean browser trust context in CI
  4. Use different tests for broken chains, bypass, and application errors
  5. Roll out certificate coverage without breaking every UI job
  6. Know when this approach is the wrong one

What you will learn

  • Separate trusted navigation from certificate bypass
  • Inspect the connection before changing browser options
  • Provision a clean browser trust context in CI
  • Use different tests for broken chains, bypass, and application errors

A login test works on a developer laptop and dies on the first navigation in CI. The browser never reaches the page because the fresh CI profile does not trust the certificate authority that signed the internal site's certificate. Turning off certificate checks makes the test green, but it also removes the behavior that needs investigation.

Internal TLS failures are awkward because several defects converge on the same browser warning. The root CA may be absent, the server may omit an intermediate, the certificate may not cover the requested hostname, or the certificate may be outside its validity period. Selenium can tell you that navigation hit an insecure certificate condition. It cannot, by itself, explain which part of the chain was wrong.

Separate trusted navigation from certificate bypass

Browsers accept an HTTPS connection only after certificate validation succeeds. At a high level, the presented leaf certificate must be valid for the requested host and time, its signature chain must lead through appropriate intermediates, and that chain must end at a trusted root. An internal CA works on the same basic model as a public CA, but its root is distributed by your organization rather than arriving in the browser's normal public trust set.

The acceptInsecureCerts WebDriver capability changes the test contract. The W3C WebDriver specification defines it as a session capability that causes certificate errors that would normally block navigation to be suppressed. Selenium exposes it through browser options, including Java's setAcceptInsecureCerts(boolean). The setting applies to the session, not to one selected URL.

That behavior has a legitimate use. A UI smoke test against a disposable environment may care only whether a button works, while TLS termination is owned by a temporary proxy outside the test's scope. Setting the capability to true can let that test reach the application. Its result must be labeled honestly: the run verifies UI behavior while certificate validation is bypassed.

The same setting is invalid for a trust test. It cannot prove that the internal root is installed, that the server sends the intermediate, that the hostname appears in the certificate, or that rotation completed cleanly. A test with bypass enabled may pass through all of those failures. Checking that the returned capability is true only proves that the bypass was requested or negotiated; it says nothing about the certificate chain.

Trusted navigation uses the opposite arrangement. Provision the intended public CA certificate in the browser's trust context, leave acceptInsecureCerts false, navigate to the internal origin, and assert an application identity after navigation. A failure then means normal certificate validation or an earlier network step prevented the document from loading. External certificate tools are still needed to distinguish chain construction, hostname, and validity problems.

The default value in the WebDriver specification is false when the endpoint supports the capability. It is still useful to set false explicitly in a certificate-focused factory because the code communicates the test's contract. After session creation, treat only a returned value of Boolean true as evidence that bypass is active. Some implementations may omit a false-valued entry, so an assertion that requires the literal object false is unnecessarily brittle.

Keep bypass and trusted sessions in different factory methods or test tags. A generic flag threaded through every test makes it too easy for certificate coverage to inherit the permissive mode. Naming the permissive method openBrowserIgnoringCertificateErrors is intentionally uncomfortable. Reviewers should see the trade-off at each call site.

Coverage is strongest when the suite has at least three contracts. A positive trust test proves that the intended chain can reach the application. A negative test proves that an untrusted endpoint is blocked. A functional smoke test may use bypass only when its report states that TLS was not evaluated. Those results answer different questions and should never be merged into one green check.

Inspect the connection before changing browser options

Begin outside Selenium. Resolve the hostname from the same network zone as the browser node, connect with Server Name Indication set to that hostname, and inspect the certificates the server actually presents. This avoids spending time on Java code when the load balancer is serving a different chain in CI than it serves on a developer network.

The following script gathers evidence without claiming that one tool models every browser decision. openssl s_client shows the peer's presented chain and verification output. openssl x509 prints the leaf certificate's subject, issuer, dates, fingerprint, and Subject Alternative Name extension. The script fails on missing inputs and writes no private key. Supply a public root certificate file that the test environment is intended to trust.

Shell
#!/usr/bin/env bash
set -euo pipefail

: "${TLS_HOST:?TLS_HOST must be set}"
: "${INTERNAL_CA_PEM:?INTERNAL_CA_PEM must point to a public CA certificate}"

TLS_PORT="${TLS_PORT:-443}"
ARTIFACT_DIR="${ARTIFACT_DIR:-tls-artifacts}"
mkdir -p "${ARTIFACT_DIR}"

set +e
openssl s_client \
  -connect "${TLS_HOST}:${TLS_PORT}" \
  -servername "${TLS_HOST}" \
  -showcerts \
  -CAfile "${INTERNAL_CA_PEM}" \
  -verify_return_error \
  </dev/null \
  >"${ARTIFACT_DIR}/connection.txt" \
  2>"${ARTIFACT_DIR}/verification.txt"
verification_status=$?
set -e

openssl s_client \
  -connect "${TLS_HOST}:${TLS_PORT}" \
  -servername "${TLS_HOST}" \
  -showcerts </dev/null 2>/dev/null \
  | openssl x509 -outform PEM \
  >"${ARTIFACT_DIR}/leaf.pem"

openssl x509 \
  -in "${ARTIFACT_DIR}/leaf.pem" \
  -noout -subject -issuer -dates -fingerprint -sha256 -ext subjectAltName

exit "${verification_status}"

Interpret each artifact narrowly. A successful OpenSSL verification shows that OpenSSL could build a valid chain under the trust material and inputs provided to that command. It does not guarantee that Firefox or Chrome uses the same trust store or chain-building behavior. A failed result proves that the command's connection and trust inputs were insufficient, but the reported depth and reason give the platform team a much better lead than a screenshot of a browser warning.

Check the hostname passed to -servername. Without it, a shared TLS endpoint may return its default certificate, producing an irrelevant mismatch. Also compare DNS results from the CI runner and, where possible, from the Grid node. A remote WebDriver session runs the browser on the node. Running OpenSSL only on the Java client can miss split DNS, a node-specific proxy, or a different load-balancer route.

The Java-side symptom depends on the browser. Firefox with geckodriver returns WebDriver's insecure certificate error, which Selenium maps to org.openqa.selenium.InsecureCertificateException. Chrome does not surface that error for a top-level navigation. ChromeDriver returns from Navigate To and leaves the browser on Chrome's own privacy interstitial, so a test written to expect a thrown exception will report a green run against a broken chain. Preserve the full exception where one exists, and assert on the interstitial where one does not. The useful stable facts are the same either way: a Navigate To command was attempted, a certificate problem was classified, no trusted application identity was asserted, and bypass was not active.

A screenshot is weak evidence at this boundary. Some browser and driver combinations return the protocol error without leaving a useful interstitial available to the test. Others may expose a warning document. Do not make screenshot presence part of the oracle. Capture the exception, requested URL, session capabilities, browser version, node identity if available, and the external chain inspection.

Driver logs also need careful interpretation. Seeing a successful New Session response proves the browser started. It does not prove the application certificate is trusted because validation happens during navigation. Conversely, failure before a session ID exists cannot be caused by the application's leaf certificate, since the browser never received the navigation command. That distinction quickly separates Grid startup failures from TLS navigation failures.

Provision a clean browser trust context in CI

Reusing a developer's browser profile makes certificate tests look easier than they are. That profile may contain an imported root, a cached intermediate, an enterprise policy, old browsing state, or a manually accepted exception. CI should build a clean, test-owned trust context from declared public certificates. The result is reproducible and disappears with the job.

Firefox can be launched by Selenium from an existing profile directory. Selenium's Java API provides FirefoxProfile(File) for constructing a profile from a directory and FirefoxOptions.setProfile for using it. On systems with Network Security Services tools available, certutil can initialize the profile's certificate database and import the public test root before Firefox starts.

The trust string and packaging policy should be reviewed by the team that owns your runner image. The example below trusts the supplied certificate as an SSL certificate authority in a temporary NSS database. It lists the imported entry so the CI artifact proves which nickname and fingerprint were present. It never imports a private key.

Shell
#!/usr/bin/env bash
set -euo pipefail

: "${INTERNAL_CA_PEM:?path to the public internal CA certificate is required}"

FIREFOX_TRUST_PROFILE="$(mktemp -d)"
cleanup() {
  rm -rf -- "${FIREFOX_TRUST_PROFILE}"
}
trap cleanup EXIT

certutil -N \
  --empty-password \
  -d "sql:${FIREFOX_TRUST_PROFILE}"

certutil -A \
  -d "sql:${FIREFOX_TRUST_PROFILE}" \
  -n "QA internal root" \
  -t "C,," \
  -i "${INTERNAL_CA_PEM}"

certutil -L \
  -d "sql:${FIREFOX_TRUST_PROFILE}" \
  -n "QA internal root"

export FIREFOX_TRUST_PROFILE
./mvnw -Dtest=InternalTlsTrustTest test

The temporary-directory cleanup is appropriate in this script because mktemp created the exact directory and the variable is quoted. In a CI workflow with separate steps, create the directory under the runner's designated temporary path and let the runner remove it after the job. Do not point cleanup at a home directory or a shared, pre-existing Firefox profile.

Create a dedicated driver for this contract. The test below leaves certificate validation enabled, verifies that bypass was not returned as active, navigates to a required internal origin, and checks a product-owned environment marker. Replace the selector and expected text with a stable marker from your application. A title or shared logo is not enough because several environments may render the same page.

Java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import java.io.File;
import java.net.URI;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.firefox.FirefoxProfile;

class InternalTlsTrustTest {
    private FirefoxDriver driver;

    @AfterEach
    void closeBrowser() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void reachesQaWithNormalCertificateValidation() {
        String profilePath = requiredEnvironment("FIREFOX_TRUST_PROFILE");
        URI applicationOrigin = URI.create(requiredEnvironment("APP_ORIGIN"));

        FirefoxProfile profile = new FirefoxProfile(new File(profilePath));
        FirefoxOptions options = new FirefoxOptions()
                .setProfile(profile)
                .setAcceptInsecureCerts(false);

        driver = new FirefoxDriver(options);

        boolean bypassActive = Boolean.TRUE.equals(
                driver.getCapabilities().getCapability("acceptInsecureCerts"));
        assertFalse(bypassActive, "certificate bypass must be disabled");

        driver.get(applicationOrigin.resolve("/health/ui").toString());
        assertEquals(
                "qa",
                driver.findElement(By.cssSelector("[data-testid='environment']"))
                        .getText());
    }

    private static String requiredEnvironment(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException(name + " is required");
        }
        return value;
    }
}

This positive test can fail if the trust database is wrong, the server chain is wrong, DNS sends the browser elsewhere, the certificate does not cover the hostname, or the application marker is wrong. That breadth is useful as a release gate, but it means the failure needs the earlier diagnostics. The assertion sequence tells you how far the run got. If the bypass assertion fails, the factory contract is broken. If driver.get throws an insecure certificate exception, focus on TLS and routing. If navigation returns and the marker fails, investigate the deployed application rather than importing more certificates.

For remote Grid execution, the profile must reach the remote browser. Selenium serializes Firefox profile data as part of Firefox options when appropriate, which adds session-request size and startup work. A platform-owned Grid image with the CA already installed may be faster and easier to rotate consistently. Whichever approach you choose, verify from a session created on the node. A certificate added only to the machine running the Java client does not automatically alter a browser running elsewhere.

Image-level trust has its own cost. The runner or node image must be rebuilt when a CA rotates, and old nodes must be drained rather than silently serving a mixed trust population. Profile-level trust makes the test input explicit but repeats profile preparation or transfer for sessions. Choose based on session volume and ownership, then record the root certificate's SHA-256 fingerprint in a non-secret artifact so mixed versions are visible.

Use different tests for broken chains, bypass, and application errors

A negative certificate test needs an endpoint whose invalid state is controlled by the test environment. Do not point it at a random public site that happens to be broken today. The platform team can expose a hostname signed by an intentionally untrusted test CA, or a disposable service can present a known fixture certificate. Keep that endpoint out of production routes and document who owns its renewal and isolation.

Browsers disagree about how that blocking surfaces, so the negative case needs two tests rather than one. Firefox 153 with geckodriver raises the W3C insecure certificate error and Selenium maps it to InsecureCertificateException. Chrome 151 does not. It renders its own privacy interstitial, and driver.get returns normally, so an assertThrows written against Chrome can never pass. The first two tests below prove the blocking on each browser in the form that browser actually produces. The third is a diagnostic demonstration of bypass behavior. It is not a substitute for the first two, and it deliberately asserts only that the page became reachable and that the session reported bypass active. Each test creates a fresh session because acceptInsecureCerts is a session capability.

The Chrome options are built in a helper for a reason worth naming. setAcceptInsecureCerts is declared on AbstractDriverOptions<DO> and returns DO, and ChromiumOptions<T extends ChromiumOptions<?>> extends AbstractDriverOptions<ChromiumOptions<?>>. The chained expression new ChromeOptions().setAcceptInsecureCerts(false) therefore has static type ChromiumOptions<?>, and assigning it to a ChromeOptions variable fails with incompatible types: ChromiumOptions<CAP#1> cannot be converted to ChromeOptions. Declare the variable first and call the setter as its own statement. The same chain compiles for Firefox only because FirefoxOptions extends AbstractDriverOptions<FirefoxOptions> directly. This is a compile-time trap, identical across Selenium 4.21 through 4.43, and it catches people who copied a Firefox snippet.

Java
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.InsecureCertificateException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;

class CertificatePolicyTest {
    private static final String UNTRUSTED_FIXTURE =
            "https://untrusted-fixture.qa.example.test/";

    private static ChromeOptions chromeOptions(boolean acceptInsecureCerts) {
        // setAcceptInsecureCerts is declared on AbstractDriverOptions<DO> and
        // returns DO. ChromiumOptions binds DO to ChromiumOptions<?>, so the
        // chained form does not yield ChromeOptions and will not compile.
        // Call the setter as its own statement.
        ChromeOptions options = new ChromeOptions();
        options.setAcceptInsecureCerts(acceptInsecureCerts);
        options.addArguments("--lang=en-US");
        return options;
    }

    @Test
    void chromeStopsAtItsInterstitialForTheControlledUntrustedFixture() {
        ChromeDriver driver = new ChromeDriver(chromeOptions(false));
        try {
            driver.get(UNTRUSTED_FIXTURE);
            String heading = driver.findElement(By.tagName("h1")).getText();
            // Read rendered text, not getPageSource(). The interstitial builds
            // its error code at runtime, so the served HTML does not contain it.
            String rendered = driver.findElement(By.tagName("body")).getText();
            assertAll(
                    () -> assertEquals("Privacy error", driver.getTitle()),
                    () -> assertEquals("Your connection is not private", heading),
                    () -> assertTrue(
                            rendered.contains("NET::ERR_CERT_AUTHORITY_INVALID"),
                            "expected the certificate error code in the interstitial"),
                    () -> assertFalse(
                            rendered.contains("tls fixture"),
                            "fixture content must stay unreachable"));
        } finally {
            driver.quit();
        }
    }

    @Test
    void firefoxRaisesTheW3cInsecureCertificateError() {
        FirefoxOptions options = new FirefoxOptions().setAcceptInsecureCerts(false);
        FirefoxDriver driver = new FirefoxDriver(options);
        try {
            assertThrows(
                    InsecureCertificateException.class,
                    () -> driver.get(UNTRUSTED_FIXTURE));
        } finally {
            driver.quit();
        }
    }

    @Test
    void bypassModeIsReportedAsBypassRatherThanTrustCoverage() {
        ChromeDriver driver = new ChromeDriver(chromeOptions(true));
        try {
            assertTrue(Boolean.TRUE.equals(
                    driver.getCapabilities()
                            .getCapability("acceptInsecureCerts")));
            driver.get(UNTRUSTED_FIXTURE);
            assertEquals(
                    "tls fixture",
                    driver.findElement(By.tagName("h1")).getText());
        } finally {
            driver.quit();
        }
    }
}

Those assertions can fail when the system under test changes. Replacing the fixture certificate with one the browser trusts breaks both negative tests at once: Firefox stops throwing, and Chrome serves the fixture page instead of a privacy interstitial, so the title, the heading, the error code, and the unreachability assertion all fail together. Disabling bypass in the third session would make its capability assertion fail. Changing the served page would fail the product assertion. This is materially different from asserting a value that was hard-coded into an object immediately before the assertion.

Two details in the Chrome test are easy to get wrong. Read the interstitial through rendered element text rather than getPageSource(), because Chrome builds the error code at runtime and the served HTML contains only the template. And pin the locale with --lang=en-US, because the title and heading are translated. If your runners cannot guarantee a locale, drop the two English string assertions and keep the NET::ERR_CERT_AUTHORITY_INVALID code, which is not localized.

Missing intermediates deserve a separate scenario. A developer's established profile may have learned or cached an intermediate from another site, while a clean CI browser cannot construct the chain from what the server sends. Evidence from s_client -showcerts reveals which certificates were presented. Compare that list with the deployment's intended chain and repeat with a clean profile. Do not fix the suite by importing the missing intermediate as a trust anchor. The server should normally present the required intermediate, while clients trust the appropriate root.

A hostname mismatch can surface through the same WebDriver error family. The requested internal alias might be orders.qa.example.test, while the leaf certificate covers only orders.internal.example.test. Importing the root does not make those names equal. Inspect the Subject Alternative Name extension from the leaf and compare it with the exact hostname after DNS and redirect processing. The repair belongs in certificate issuance, routing, or the test URL, depending on which name is authoritative.

Validity failures also look similar. A certificate can be expired, not yet valid because of a bad issuance window, or apparently invalid on a node whose clock is wrong. Record the leaf's validity dates and the node's UTC time. Do not publish a token or private key in that artifact. If only one Grid node fails, compare its clock, browser version, proxy path, and trust fingerprint with a passing node before rotating certificates globally.

An HTTP application failure is a near-miss that should not be classified as TLS. WebDriver navigation can return a document for an HTTP 404, 500, or proxy-generated error page because those are HTTP responses, not necessarily transport errors. Selenium does not expose the navigation status through the basic get call. Assert a product marker, and use service or proxy logs to identify the status. Adding a CA cannot turn a correctly encrypted 502 page into a healthy application.

Mutual TLS is another distinct boundary. Installing a root certificate lets the browser validate the server. It does not supply a client certificate when the server requires one. A handshake failure at an mTLS gateway needs client-certificate provisioning and a test design that protects that credential. Do not describe acceptInsecureCerts as an mTLS solution; it changes server certificate handling, not client identity.

Corporate interception proxies can invert the diagnosis. The internal service may present the expected chain from one network, while the Grid node sees a certificate issued by a proxy CA. Capture issuer and fingerprint from the node's path. If interception is intended, its public root needs an explicit trust and governance decision. If it is not intended for that host, bypass would conceal a routing or security-policy defect.

A CA rotation creates one more failure that often gets mislabeled as random Grid instability. Imagine that the service has switched to a chain ending at the new root, but only half of the browser nodes received the updated trust image. Tests alternate between passing and an insecure certificate error even though every request uses the same hostname. A retry appears to cure the problem because Grid schedules the next session on a different node.

Correlate each attempt with its node or image identity and the public root fingerprint installed there. The pattern becomes clear when failures cluster on the old image rather than on one test method or application release. Increasing the navigation timeout cannot help, and enabling bypass would turn a partially deployed trust change into a green result. Finish the trust rollout or drain the stale nodes, then repeat the strict canary on each node class.

The reverse sequence can fail too. Removing the old root from every node before the service stops serving the old chain creates a clean outage instead of an intermittent one. Coordinate an overlap window in which clients trust both approved roots, verify both intended chain variants, then remove the retired root. The overlap is not permission to trust arbitrary certificates. Keep the accepted fingerprints explicit and time-bound.

Issuer display names are not sufficient evidence during this work. Two CA certificates can share a human-readable subject while containing different public keys. Record SHA-256 fingerprints and inspect the actual chain rather than concluding that similarly named roots are interchangeable. This is also why a log line saying only issuer=Internal QA CA cannot prove which trust material a browser used.

Roll out certificate coverage without breaking every UI job

Inventory the existing suite before enforcing trust. Record which hostnames it visits, which runs currently set acceptInsecureCerts, whether browsers are local or remote, and where trust material is provisioned. Include identity-provider and asset hosts that are part of navigation, not just the initial application origin. A strict main page can still fail later when a frame or redirect reaches a differently configured internal host.

Create one trust canary per browser and execution image. It should open a stable internal page with bypass disabled and assert a non-secret environment marker. Run the canary before the broad UI suite. When it fails, stop that shard or classify it as environment setup rather than allowing hundreds of tests to produce identical navigation errors.

Move permissive tests into an explicit group. Name the job so reports say ui-smoke-tls-bypassed, attach the effective capability, and keep it out of certificate quality metrics. This preserves product feedback while the platform team repairs trust distribution. Give the exception an owner and removal condition. A permanent unlabeled bypass becomes invisible debt.

The following workflow illustrates the wiring for a Firefox trust canary on a Linux runner. The public CA is stored as encoded CI data, decoded only into the runner's temporary directory, and imported into a fresh profile. The workflow then runs the dedicated Java test with bypass disabled in code. Pin action revisions according to your organization's supply-chain policy rather than copying versions blindly.

YAML
name: internal-tls-canary

on:
  workflow_dispatch:

jobs:
  firefox-trust:
    runs-on: ubuntu-latest
    env:
      APP_ORIGIN: https://qa.example.test
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
          cache: maven

      - name: Install NSS certificate tools
        run: |
          sudo apt-get update
          sudo apt-get install --yes libnss3-tools

      - name: Create Firefox trust profile
        env:
          INTERNAL_CA_PEM_B64: ${{ secrets.INTERNAL_CA_PEM_B64 }}
        run: |
          set -euo pipefail
          profile_dir="${RUNNER_TEMP}/firefox-trust-profile"
          ca_file="${RUNNER_TEMP}/internal-root.pem"
          mkdir -p "${profile_dir}"
          printf '%s' "${INTERNAL_CA_PEM_B64}" | base64 --decode >"${ca_file}"
          certutil -N --empty-password -d "sql:${profile_dir}"
          certutil -A -d "sql:${profile_dir}" -n "QA internal root" -t "C,," -i "${ca_file}"
          printf 'FIREFOX_TRUST_PROFILE=%s\n' "${profile_dir}" >>"${GITHUB_ENV}"

      - name: Run trusted-navigation canary
        run: ./mvnw -Dtest=InternalTlsTrustTest test

Plan CA rotation as an overlap, not an instantaneous replacement. During a planned transition, the service chain and trust images may not change at exactly the same moment. Publish the accepted root fingerprints for the transition window, test each intended chain explicitly, and remove the old root after the service and node fleet converge. Do not accept any certificate merely to avoid coordinating the overlap.

Profile transfer and image rebuilds add latency. A large custom Firefox profile costs time for every remote session, so keep it minimal or provision trust in the Grid image. A canary adds another browser startup before the suite. That is a concrete cost, but it is usually cheaper than launching an entire shard that cannot navigate anywhere. Measure the cost in your own CI rather than copying illustrative timing numbers from another system.

Certificate artifacts need retention rules. Public certificates are not private keys, but internal hostnames, topology, and issuer names may still be operationally sensitive. Store the minimum required chain details with the run, restrict artifact access, and avoid dumping full environment maps. The best diagnostic package contains the requested host, UTC time, public certificate fingerprints and metadata, browser version, effective bypass state, node identity, and original exception.

Know when this approach is the wrong one

Do not import an internal root for a public production hostname that is supposed to use public trust. Making the test runner trust a private replacement would allow the test to pass while real customers receive a certificate warning. The trust configuration must match the user population whose experience the suite claims to represent.

Avoid a custom browser profile when enterprise policy already provisions and audits trust on every Grid node. Duplicating the CA in a test-owned profile creates two rotation paths and makes failures harder to attribute. In that environment, leave validation enabled, record the node image or policy version, and let the platform-managed store be the source of truth.

Do not use Selenium as the only certificate monitor. Browser automation is valuable because it exercises the real navigation path and catches integration problems, but it is slower and less precise than dedicated TLS checks for expiry, chain contents, and hostname coverage. Pair a small WebDriver canary with platform monitoring. Let each tool report the layer it actually observes.

Skip bypass when the test asserts any security property influenced by TLS. Login, cookie security, redirect policy, mixed-content behavior, client certificates, certificate warnings, and transport downgrade checks all depend on a realistic secure context. A green result from a session that accepts invalid certificates overstates coverage.

A bypass may be reasonable for short-lived UI feedback when certificate behavior is expressly out of scope and the environment is isolated. Make that choice per job, use a fresh session, label the report, and prevent the permissive factory from being imported by trust tests. The cost is reduced realism and the possibility of hiding a routing defect, so keep a separate strict canary even if the larger smoke suite remains permissive.

Finally, stop editing Selenium configuration when the evidence identifies a server defect. A missing intermediate, wrong Subject Alternative Name, expired leaf, or unexpected proxy issuer belongs to the certificate or network owner. Hand them the connection artifact, node location, requested hostname, time, and certificate fingerprints. Keeping bypass disabled in the canary ensures the next run will verify the repair instead of merely reaching the page around it.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official w3.org reference

    w3.org

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official developer.mozilla.org reference

    developer.mozilla.org

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Why does an internal HTTPS site open manually but fail in Selenium?

The manual browser profile may already trust the company CA, while WebDriver starts with a fresh profile. Compare the certificate stores and run the same URL with a clean manual profile before changing Selenium options.

Does acceptInsecureCerts install my internal CA?

No. That capability tells the WebDriver session to tolerate invalid or untrusted certificates during navigation; it does not establish that the presented chain reaches your intended trust anchor.

How can I tell a missing intermediate from a missing root certificate?

Inspect the chain sent by the server and verify the leaf with the intended root plus any supplied intermediate certificates. A fresh browser profile is useful because an existing profile may have cached an intermediate.

Should every Selenium test run with certificate checks enabled?

Functional suites should normally use a correctly provisioned trust store, while dedicated negative TLS tests must keep normal validation enabled. A narrowly labeled smoke job may accept insecure certificates when TLS is explicitly outside its scope, but it should not be reported as certificate coverage.

Can the internal CA private key be placed on the CI runner?

Only the public CA certificate is needed to establish trust. Keep the CA private key off test runners because possession of it would allow certificate issuance and creates a much larger security risk.