Skip to content

Add: Exact client certificate pinning for openvasd mTLS#2250

Open
mde-gb wants to merge 17 commits into
mainfrom
pinned-mtls-client-certs
Open

Add: Exact client certificate pinning for openvasd mTLS#2250
mde-gb wants to merge 17 commits into
mainfrom
pinned-mtls-client-certs

Conversation

@mde-gb

@mde-gb mde-gb commented Jun 17, 2026

Copy link
Copy Markdown

What

Adds support for pinned (exact) client leaf certificates for openvasd mTLS,
alongside the existing CA-based client certificate validation.

  • New [tls] option pinned_client_certs (file or directory of client leaf
    certs), with CLI flag --tls-pinned-client-certs and env
    TLS_PINNED_CLIENT_CERTS.
  • A PinnedClientCertVerifier (rustls ClientCertVerifier) that authenticates
    a client when its leaf certificate exactly matches a configured/registered
    certificate — without trusting the issuing CA for any other client.
  • Pinned leaves are still checked for time validity (notBefore/notAfter), are
    rejected if they are CA certificates, must carry the clientAuth extended
    key usage, and must allow digitalSignature when a KeyUsage extension is
    present.
  • client_certs (CA-based) and pinned_client_certs can be combined: CA trust
    keeps working as before, while pinned certs are accepted in addition.
  • Legacy client_certs behaviour for missing or empty directories is
    preserved, and symlinked directories or broken symlinks are not treated as
    client certificate files. pinned_client_certs fails startup when the
    configured path is missing, invalid, or contains no certificate files.
  • Client authentication settings require complete server TLS configuration and
    fail startup with an explicit error otherwise.
  • Docs (openvasd/README.md, config.example.toml), tests, test fixtures, and
    a pinned-mtls CI smoke workflow.

Incidental cleanups pulled in while building this: an OS-detection test now uses
a NamedTempFile instead of a predictable /tmp/os-release path, a trailing
whitespace fix in a SQL string, and the self-signed client certificate example
now uses the client certificate extension profile.

Why

Pinned client certificates let an operator authenticate clients that cannot or
should not share a common CA
:

  • No PKI required. A client organization can authenticate with a single
    self-signed certificate. There is no intermediate/root CA to run, distribute,
    or rotate — lowering the barrier for smaller deployments and integrations.
  • Tighter trust scope. CA-based mTLS trusts every certificate the CA ever
    issues. Pinning trusts exactly the enrolled leaf and nothing else, so a
    compromised or over-permissive CA cannot mint additional accepted clients.
    This is a least-privilege / reduced-blast-radius improvement.
  • Heterogeneous clients. Clients signed by different CAs (or self-signed)
    can be onboarded individually without forcing them under one shared CA.
  • Defense in depth, not a weakening. Pinned leaves are still validated for
    time validity, rejected if they are CA certs, required to have the
    clientAuth EKU, and checked for signing-capable KeyUsage when present -
    so pinning adds an authentication mode without loosening the existing one. CA
    mode is unchanged and remains supported.

How

  • build_client_cert_verifier decides the verifier based on configuration:
    • CA certs only → existing WebPkiClientVerifier (unchanged behaviour).
    • Pinned certs present → PinnedClientCertVerifier, which first tries the CA
      verifier (when CA certs are also configured) and otherwise falls back to an
      exact DER match against the pinned set, followed by validity/CA/EKU/KeyUsage
      checks.
  • Signature verification (TLS 1.2/1.3) is delegated to a webpki-backed verifier
    built over the union of CA + pinned roots, so handshake signature checks keep
    using vetted rustls logic.
  • Config plumbing: pinned_client_certs flows from TOML/CLI/env →
    RunnerBuilder::path_pinned_client_certstls_config.
  • Tests cover CA-only, pinned-only, mixed CA+pinned, expired pinned certs,
    non-clientAuth certs, missing digitalSignature, pinned CA certificates,
    empty pinned cert directories, symlinked directories, and rejection of unknown
    clients. A smoke workflow generates ephemeral certs and exercises the path end
    to end in CI.

IMPORTANT — contributing checklist

  1. RELICENSE: included (RELICENSE/mde-gb.md) — first contribution.
  2. Conventional commits: PR titled Add: (new feature → minor release).
  3. Generative AI disclosure: Yes. Parts of the certificate-validation logic,
    tests, and this PR description were drafted with AI assistance (Codex /
    GitHub Copilot) and reviewed by a human before submission. See the
    Co-authored-by: AI (codex/partial) trailer on the relevant commit.

Jira: SC-1629

Support pinning exact client certificates for openvasd mTLS in addition to
CA-based client trust. Pinned client leaves are validated for time validity
and clientAuth extended key usage, and CA hints are only sent when CA-based
client trust is configured.

Also includes supporting changes:
- Update x509-parser to 0.18.1
- Add pinned mTLS smoke-test workflow and client-auth test certificates
- Restore arm64 Rust builds on pull requests and keep dependency review
  fail-closed
- Add RELICENSE entry for mde-gb

Co-authored-by: AI (codex/partial)
@mde-gb mde-gb requested a review from a team as a code owner June 17, 2026 12:37
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
See the Details below.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA ca5207c.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

License Issues

.github/workflows/openvasd-pinned-mtls-smoke.yml

PackageVersionLicenseIssue Type
greenbone/actions/checkout3.*.*NullUnknown License
Allowed Licenses: 0BSD, AGPL-3.0-or-later, Apache-2.0, BlueOak-1.0.0, BSD-2-Clause, BSD-3-Clause-Clear, BSD-3-Clause, BSL-1.0, bzip2-1.0.6, CAL-1.0, CC-BY-3.0, CC-BY-4.0, CC-BY-SA-4.0, CC0-1.0, EPL-2.0, GPL-1.0-or-later, GPL-2.0-only, GPL-2.0-or-later, GPL-2.0, GPL-3.0-only, GPL-3.0-or-later, GPL-3.0, ISC, LGPL-2.0-only, LGPL-2.0-or-later, LGPL-2.1-only, LGPL-2.1-or-later, LGPL-2.1, LGPL-3.0-only, LGPL-3.0, LGPL-3.0-or-later, MIT, MIT-CMU, MPL-1.1, MPL-2.0, OFL-1.1, PSF-2.0, Python-2.0, Python-2.0.1, Unicode-3.0, Unicode-DFS-2016, Unlicense, Zlib, ZPL-2.1

OpenSSF Scorecard

PackageVersionScoreDetails
actions/greenbone/actions/checkout 3.*.* UnknownUnknown

Scanned Files

  • .github/workflows/openvasd-pinned-mtls-smoke.yml
  • rust/Cargo.toml

@github-actions github-actions Bot added the minor_release creates a minor release label Jun 17, 2026
@mergify

mergify Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

⚠️ The sha of the head commit of this PR conflicts with #2248. Mergify cannot evaluate rules on this PR. Once #2248 is merged or closed, Mergify will resume processing this PR. ⚠️

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for exact (pinned) client leaf certificates for openvasd mTLS, allowing client authentication by matching the presented leaf certificate DER against a configured set, optionally alongside existing CA-based client cert validation.

Changes:

  • Introduces a PinnedClientCertVerifier (rustls ClientCertVerifier) and config plumbing to accept pinned client leaf certs in addition to (or instead of) CA-based validation.
  • Adds CLI/env/config/docs for pinned_client_certs, plus new test fixtures and a CI smoke workflow validating pinned-mTLS behavior.
  • Includes small incidental cleanups (tempfile usage in tests, SQL whitespace tweak) and bumps x509-parser to 0.18.1.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
rust/src/openvasd/README.md Documents pinned client cert option and CLI flag for openvasd mTLS.
rust/src/openvasd/main.rs Wires pinned_client_certs from config into the runtime builder.
rust/src/openvasd/container_image_scanner/scheduling/db/sqlite/images.rs Trims trailing whitespace in a SQL string.
rust/src/openvasd/container_image_scanner/detection.rs Makes OS-detection test use NamedTempFile instead of a fixed /tmp path.
rust/src/openvasd/config/mod.rs Adds [tls].pinned_client_certs plus CLI/env handling.
rust/examples/tls/self-signed/client_certificates.sh Adjusts OpenSSL extension section used for client cert generation.
rust/examples/openvasd/config.example.toml Adds example/commentary for pinned_client_certs.
rust/crates/greenbone-scanner-framework/src/tls.rs Implements pinned-leaf verification and updates TLS config assembly; adds unit tests.
rust/crates/greenbone-scanner-framework/src/test-data/pinned-client.pem Adds pinned client cert fixture.
rust/crates/greenbone-scanner-framework/src/test-data/other-client.pem Adds alternate client cert fixture.
rust/crates/greenbone-scanner-framework/src/test-data/ca.pem Adds CA cert fixture used by tests.
rust/crates/greenbone-scanner-framework/src/lib.rs Extends TLS config/state to carry pinned client cert path and treat it as mTLS auth.
rust/crates/greenbone-scanner-framework/Cargo.toml Adds x509-parser dependency to the framework crate.
rust/Cargo.toml Bumps/introduces workspace dependency for x509-parser.
rust/Cargo.lock Updates lockfile for x509-parser bump and new dependency edges.
RELICENSE/mde-gb.md Adds relicensing/DCO document for the contribution.
.github/workflows/openvasd-pinned-mtls-smoke.yml Adds CI smoke workflow exercising pinned-mTLS end-to-end.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rust/src/openvasd/README.md Outdated
Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
Comment thread rust/Cargo.toml
Comment thread RELICENSE/mde-gb.md Outdated
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 17, 2026
The pinned mTLS smoke workflow previously built the binary in a separate
job via the reusable build-rs.yaml workflow and passed it to the smoke
job using actions/upload-artifact and actions/download-artifact. The
dependency review flagged actions/download-artifact (and actions/checkout)
with an unknown license.

Inline the same Docker build directly in the smoke job and resolve the
binary from the local build output, removing the cross-job artifact
handoff entirely. This drops the actions/download-artifact dependency and
its license warning without relying on greenbone/actions/download-artifact,
which fetches artifacts from a separate completed run via the GitHub API
and does not fit a same-run handoff.

- Remove the build job and its dependency on build-rs.yaml
- Build openvasd inline with the same target, build args, and GHA cache
- Resolve the binary from ./dist-amd64 instead of the downloaded artifact
- Drop the build-rs.yaml path trigger

Co-authored-by: AI (copilot/full)
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 17, 2026
- Reword openvasd README to "registered clients'" leaf certificates
- Dedupe x509-parser to the workspace dependency in scannerlib
- Rename the CA-rejection test to reflect what it asserts
- Add a mixed-mode test: pinned leaf from an untrusted CA is accepted
  while a sibling leaf from that CA is rejected
- Fill the MIT-0 copyright placeholder in RELICENSE/mde-gb.md

Co-authored-by: AI (copilot/partial)
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 17, 2026
@mde-gb mde-gb requested a review from Copilot June 17, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 5 comments.

Comment thread rust/src/openvasd/config/mod.rs
Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
- Clarify --tls-pinned-client-certs help: path may be a file or directory
- Reword load_client_cert_paths errors to "client authentication
  certificate" since the path may hold CA certs, not only client leaves
- Add a negative test: a pinned leaf without the clientAuth EKU is
  rejected (new no-client-auth-client.pem fixture, CA:FALSE + serverAuth)

Co-authored-by: AI (copilot/partial)
@mde-gb mde-gb requested a review from Copilot June 17, 2026 13:41
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 20 changed files in this pull request and generated 1 comment.

Comment thread rust/src/openvasd/main.rs Outdated
Only apply client certificate and pinned client certificate paths after both server TLS certificate and key are configured. This prevents client authentication settings from creating a TLS runtime with empty server certificate paths when openvasd should run without TLS.

Co-authored-by: AI (codex/partial)
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 18, 2026
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 18, 2026
@mde-gb mde-gb requested a review from Copilot June 18, 2026 10:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 1 comment.

Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs Outdated
Include directory and entry paths when reading pinned client certificate directories fails, so startup errors identify the problematic path. Add a regression test for metadata failures caused by broken symlink entries.

Co-authored-by: AI (codex/partial)
@mde-gb mde-gb requested a review from Copilot June 18, 2026 11:05
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 1 comment.

Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs
mde-gb added 2 commits June 18, 2026 13:15
Apply rustfmt output for pinned client certificate tests so cargo fmt --check passes in CI.

Co-authored-by: AI (codex/partial)
Follow symlink metadata before accepting legacy client_certs entries as certificate files. This avoids treating symlinked directories or broken symlinks as candidate CA certificates while keeping the existing non-recursive loading behavior.

Co-authored-by: AI (codex/partial)
@mde-gb mde-gb requested a review from Copilot June 18, 2026 11:21
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comment thread rust/src/openvasd/README.md Outdated
Comment thread rust/src/openvasd/README.md Outdated
Comment thread rust/src/openvasd/main.rs Outdated
Document that pinned client certificate configuration accepts a file or directory and centralize the startup error for client authentication without complete server TLS.

Co-authored-by: AI (codex/partial)
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 18, 2026
@mde-gb mde-gb requested a review from Copilot June 18, 2026 11:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 1 comment.

Comment thread rust/crates/greenbone-scanner-framework/src/tls.rs Outdated
Ignore unreadable entries while scanning pinned client certificate directories, so broken symlinks are not treated as certificate files. Keep failing startup when no usable pinned certificate files remain.

Co-authored-by: AI (codex/partial)
@mde-gb mde-gb requested a review from Copilot June 18, 2026 12:09
@github-actions github-actions Bot added minor_release creates a minor release and removed minor_release creates a minor release labels Jun 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 2 comments.

Comment on lines +380 to +383
let root_hint_subjects = ca_verifier
.as_ref()
.map(|verifier| verifier.root_hint_subjects().to_vec())
.unwrap_or_default();
smoke:
runs-on: ubuntu-latest
steps:
- uses: greenbone/actions/checkout@v3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor_release creates a minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants