Skip to content

fix: check the hostname of each OpenStack endpoint's certificate - #31

Merged
atoomic merged 2 commits into
cpan-authors:mainfrom
teodesian:verify-tls-hostname
Sep 20, 2026
Merged

atoomic merged 2 commits into
cpan-authors:mainfrom
teodesian:verify-tls-hostname

Conversation

@teodesian

@teodesian teodesian commented Sep 18, 2026 •

Copy link
Copy Markdown

OpenStack::Client makes each of its user agents with ssl_opts => { verify_hostname => 0 }, and no argument changes that. So every request from OpenStack::MetaAPI, and the X-Auth-Token in it, goes to whichever server answers for the endpoint. The last release of OpenStack::Client is from 2019, and the repository in its metadata no longer exists, so the fix cannot go there.

OpenStack::Client does let a caller name the class that it calls new on, as package_ua, and OpenStack::Client::Auth passes that to every client it makes. This PR uses that:

  • OpenStack::MetaAPI::UserAgent is an LWP::UserAgent that removes verify_hostname from the ssl_opts it is given. LWP then decides as it does for any user agent: the check is on, unless PERL_LWP_SSL_VERIFY_HOSTNAME is 0. PERL_LWP_SSL_CA_FILE and PERL_LWP_SSL_CA_PATH name a private certificate authority.
  • new() gives OpenStack::Client::Auth that class as package_ua, unless the caller names another one.
  • A caller that builds its own auth object (the path that Add several features missing from OpenStack::MetaAPI #28 added) is not changed. The POD of the class says what to pass.

This changes behavior for a cloud with a self-signed certificate. Such a cloud now needs PERL_LWP_SSL_CA_FILE, or PERL_LWP_SSL_VERIFY_HOSTNAME=0. The Changes entry says so.

LWP::UserAgent moves from a test prerequisite to a runtime prerequisite, in dist.ini, Makefile.PL and .github/cpanfile.

Testing

prove -l t/ passes: 22 files, 271 tests. t/useragent.t is new. It shows that OpenStack::Client alone turns the check off, and that this class turns it on and keeps the other ssl_opts. It shows that PERL_LWP_SSL_VERIFY_HOSTNAME=0 turns the check off again. It also shows that new() passes the class to OpenStack::Client::Auth, and that a caller's own package_ua wins. The last test fails against main's MetaAPI.pm.

AI assistance

Per AI_POLICY.md: Claude Code wrote this change and its tests, at my request.

🤖 Generated with Claude Code

OpenStack::Client makes every user agent with
ssl_opts => { verify_hostname => 0 }, and no argument changes it.  So
each request, and the X-Auth-Token in it, went to whichever server
answered for the endpoint.

OpenStack::Client does let a caller name the class it calls new on, as
package_ua.  OpenStack::MetaAPI::UserAgent is an LWP::UserAgent that
removes verify_hostname from the ssl_opts it is given, so LWP decides as
it does for any user agent: the check is on unless
PERL_LWP_SSL_VERIFY_HOSTNAME is 0.  new() gives it to
OpenStack::Client::Auth unless the caller names another package_ua.

A caller that builds its own auth object is not changed, and the POD of
the class says what to pass.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@Koan-Bot

Koan-Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@Koan-Bot Koan-Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Important issues found.

  • New SYNOPSIS fails the dzil-generated xt/author/synopsis.t (strict vars)

The SYNOPSIS of OpenStack::MetaAPI::UserAgent used $endpoint and
%auth_args without a declaration, and declared $api twice. The
xt/author/synopsis.t that [Test::Synopsis] makes compiles it under
strict, so dzil test and dzil release failed. A =for test_synopsis
block now declares the variables.

LWP also turns the hostname check off when HTTPS_CA_FILE or
HTTPS_CA_DIR is set and PERL_LWP_SSL_VERIFY_HOSTNAME is not. The POD
and the Changes entry now say so, and t/useragent.t tests it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@Koan-Bot

Copy link
Copy Markdown
Contributor

PR Review — fix: check the hostname of each OpenStack endpoint's certificate

Restores TLS hostname verification by routing OpenStack::Client's package_ua hook through a thin LWP::UserAgent subclass that drops the hardcoded verify_hostname => 0; both findings from the previous round are addressed, and what remains is the unverified assumption that the declared minimum OpenStack::Client honours that hook.

  • Prior synopsis failure is fixed — =for test_synopsis declares $endpoint/%auth_args and survives PodWeaver's [Leftovers] pass
  • POD and the Changes entry now name HTTPS_CA_FILE/HTTPS_CA_DIR as the second way the check goes off
  • The ssl_opts copy leaves the caller's hash untouched, and the test asserts exactly that
  • BUILDARGS rewrite preserves every existing call form — t/01_base.t's hashref cases still reach the same Auth errors
  • Propagation through OpenStack::Client::Auth is assumed, not tested, and the prereq floor is still 1.0006

✅ Resolved since last review (1)

Previously-flagged issues verified fixed
  • lib/OpenStack/MetaAPI/UserAgent.pm:52 POD names only PERL_LWP_SSL_VERIFY_HOSTNAME; HTTPS_CA_FILE/HTTPS_CA_DIR also disable the check

🟢 Suggestions

1. The one link the guarantee depends on — Auth propagating `package_ua` — is the only one not tested
t/useragent.t:66-69

t/useragent.t covers each end of the chain well: OpenStack::Client->new($ENDPOINT, package_ua => ...) really does build the subclass (line 59-64), and new() really does put package_ua in the args (line 66-81). The middle link is the one the security property actually rests on — OpenStack::Client::Auth passing that class to the Keystone token UA and to every per-service client it constructs — and it is mocked out by $mock->redefine(new => ...), so nothing in the suite would notice if it stopped happening.

That matters because OpenStack::Client is unmaintained and pinned only by a minimum version, so the propagation is an assumption about a third-party module rather than about code in this repo.

t/01_base.t's get_api_object() already builds a real OpenStack::Client::Auth::v3 against the mocked LWP, so the assertion is cheap to add there — e.g. check that the compute service client's {ua} isa OpenStack::MetaAPI::UserAgent, which fails loudly if a future OpenStack::Client drops the hook.

    my $mock = Test::MockModule->new('OpenStack::Client::Auth');
    $mock->redefine(new => sub { shift; @got = @_; return bless {}, 'Test::Auth' });

Checklist

  • Security fix covers the full request path (token + service clients)
  • BUILDARGS refactor preserves existing call forms
  • New behavior is covered by tests — suggestion #1
  • Author/release test suite still builds (synopsis under strict vars)
  • Documentation accurately states the security guarantee
  • Backward-incompatible behavior change is recorded in Changes
  • Prerequisites match what the code depends on
ℹ️ Triage summary

2 pre-existing finding(s) on unchanged code suppressed (freeze).


Automated review by Skuggi HEAD=04f1382 5 min 54s

@Koan-Bot Koan-Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tip

No blocking issues found — ready to merge.

@atoomic
atoomic merged commit 375a61b into cpan-authors:main Sep 20, 2026
19 checks passed
@atoomic

atoomic commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks @teodesian this is now released as OpenStack-MetaAPI-0.005

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants