NETWAYS / NETWAYS/ansible-collection-elasticstack

[Bug]: The SSLContext in api.py has no effect, and the logstash user is created unverified

Open
#549 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug component:plugins
Dominant language
Jinja
Stars
14
Forks
11
Avg merge
1d 47m
Merged PRs (30d)
6

Description

Component

plugins/module_utils/api.py, and the two module calls in roles/logstash/tasks/logstash-security.yml.

What happens

Api.new_client_basic_auth() builds an SSLContext, switches both checks off, and hands it to the client next to verify_certs:

ctx = ssl.create_default_context(cafile=ca_certs)
ctx.check_hostname = False
ctx.verify_mode = False

return Elasticsearch(
    hosts=[host],
    basic_auth=(auth_user, auth_pass),
    ssl_context=ctx,
    verify_certs=verify_certs
)

NodeConfig.__post_init__ rejects ssl_context together with any other TLS option, and its comment states the contract: the context has to be configured the way the caller wants it already. verify_certs is the one option missing from that list, so our combination is accepted instead of rejected, and elastic_transport then configures the urllib3 pool from verify_certs no matter what the context says. Measured pool state with verify_certs: true:

pool.cert_reqs       = 'CERT_REQUIRED'
pool.ca_certs        = <certifi>/cacert.pem
pool.assert_hostname = None
pool.conn_kw         = ['ssl_context']

So verify_mode is overwritten, CERT_NONE becomes CERT_REQUIRED on the first request.

check_hostname = False does not switch the name check off, it switches urllib3's own one on. urllib3/connection.py:993-1014 calls _match_hostname exactly when the context verifies but does not check the name itself:

elif (
    context.verify_mode != ssl.CERT_NONE
    and not context.check_hostname
    and assert_hostname is not False
):

All three hold here, so the name is checked and a certificate on the wrong name is rejected. verify_certs: true therefore verifies both chain and name.

ca_certs never reaches the client, only create_default_context(), so NodeConfig.ca_certs stays None and elastic_transport substitutes its default. certifi (121 CAs) ends up in the same context as our stack CA, measured 1 CA before the first request and 122 after.

An http:// host cannot work at all. The context is built unconditionally, and the same check rejects any TLS option on a non-https scheme:

ValueError: TLS options require scheme to be 'https'

raised in _models.py:327, at client construction, before a single request. Without the context an http host is fine even with verify_certs=True, because the whole TLS block in _http_urllib3.py sits inside if config.scheme == "https" and verify_certs is never read for http. This blocks elasticsearch_http_security: false, which #550 covers on the role side.

The two calls in the logstash role also disagree for the same connection to the same host. Line 405 creates the role with verify_certs: true, line 422 creates the user with verify_certs: false. Both pass the same ca_certs, and both came in together with #323.

How to reproduce

Serve a certificate issued for a name that does not match, then connect the way the module does.

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 1 -nodes \
  -subj "/CN=wrong-name.example" -addext "subjectAltName=DNS:wrong-name.example"
import ssl
from elasticsearch import Elasticsearch

ctx = ssl.create_default_context(cafile="cert.pem")
ctx.check_hostname = False
ctx.verify_mode = False
print("before:", len(ctx.get_ca_certs()), ctx.verify_mode)

c = Elasticsearch(hosts=["https://localhost:19443"], basic_auth=("elastic", "secret"),
                  ssl_context=ctx, verify_certs=True)
try:
    c.info()
except Exception as e:
    print("rejected:", e)
print("after: ", len(ctx.get_ca_certs()), ctx.verify_mode)
Relevant output
before: 1 VerifyMode.CERT_NONE
rejected: TLS error caused by: SSLError(hostname 'localhost' doesn't match 'wrong-name.example')
after:  122 VerifyMode.CERT_REQUIRED

Setups do not run into the name mismatch, because elasticsearch-certutil is called with --ip {{ ansible_default_ipv4.address }} and the logstash role builds host from that same address.

Environment

elasticsearch-py 8.19.3 with elastic_transport 8.19.0 and urllib3 2.7.0, which is what our elasticsearch<9 pin resolves to.

What we want
  • create the logstash user over a verified connection, like the role above it
  • drop the SSLContext and use the client's own TLS options, set only for an https host
kwargs = {}
if host.startswith('https://'):
    kwargs['verify_certs'] = verify_certs
    if verify_certs and ca_certs:
        kwargs['ca_certs'] = ca_certs

return Elasticsearch(
    hosts=[host],
    basic_auth=(auth_user, auth_pass),
    **kwargs
)

Verified against a local TLS server: an https host with a wrong name is still rejected, verify_certs: false still connects, and an http host works.

Narrowing the trust store to our CA alone is not achievable and should not be part of this. Whenever verify_certs is true, elastic_transport hands urllib3 a ca_certs path, ours or certifi as the default, and urllib3 loads it into the context in addition to whatever is already there. Four variants measured:

construction CAs in the trust store result
ssl_context(cafile=ca) alone 122 connects
ssl_context(cafile=ca) + verify_certs=False 1 breaks, Cannot set verify_mode to CERT_NONE when check_hostname is enabled
ca_certs=ca + verify_certs=True 186 connects
empty SSLContext with only our CA loaded 122 connects

122 is our CA plus certifi's 121, 186 is the host's system store plus our CA. The practical exposure is small, publicly trusted CAs may not issue for reserved IP addresses and we connect by private IP, so this is worth a note in the module documentation rather than a fix.

Related: #513 does the same thing in the beats Elasticsearch output.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with plugins/module_utils/api.py and the two Elasticsearch module calls in roles/logstash/tasks/logstash-security.yml. Reproduce the behavior with the local TLS server described in the issue, checking HTTPS verification, the ca_certs option, and an HTTP host. Done means the role creates the user over a verified connection, HTTPS and HTTP hosts behave as specified, and the SSLContext is no longer used.

Written by the indexing model from the issue text.

Assessment

Tech stack
ansible, elasticsearch, python
Domain
devops, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.