Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ProxySQL check to use TLS context wrapper #8243

Merged
merged 15 commits into from
Jan 4, 2021
12 changes: 12 additions & 0 deletions proxysql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ The ProxySQL integration is included in the [Datadog Agent][3] package, so you d

### Configuration

#### Enabling SSL
To connect to ProxySQL using SSL/TLS, enable the `use_tls` option in `conf.yaml`. Include certificates and passwords needed to connect via SSL/TLS.

```yaml
use_tls: true
tls_cert: cert.pem
tls_ca_cert: ca_cert.pem
tls_private_key: private_key.pem
yzhan289 marked this conversation as resolved.
Show resolved Hide resolved
tls_private_key_password: Password123

```

<!-- xxx tabs xxx -->
<!-- xxx tab "Host" xxx -->

Expand Down
20 changes: 6 additions & 14 deletions proxysql/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,15 @@ files:
value:
type: string
example: <PROXYSQL_ADMIN_PASSWORD>
- name: tls_verify
value:
example: false
type: boolean
description: Instructs the check to use SSL when connecting to ProxySQL
- name: tls_ca_cert
value:
example: <CA_CERT_PATH>
type: string
- name: use_tls
required: false
description: |
The path to the `proxysql-ca.pem` file generated by ProxySQL when using SSL (or your own).
More information: https://github.com/sysown/proxysql/wiki/SSL-Support#ssl-configuration-for-frontends
- name: validate_hostname
Instructs the check to use SSL when connecting to ProxySQL
value:
example: true
type: boolean
description: Whether or not to verify the certificate was issued for the given `host`
example: false
default: false
- template: instances/tls
- name: connect_timeout
required: false
description: |
Expand Down
47 changes: 40 additions & 7 deletions proxysql/datadog_checks/proxysql/data/conf.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,54 @@ instances:
#
password: <PROXYSQL_ADMIN_PASSWORD>

## @param tls_verify - boolean - optional - default: false
## @param use_tls - boolean - optional - default: false
## Instructs the check to use SSL when connecting to ProxySQL
#
# tls_verify: false
# use_tls: false

## @param tls_verify - boolean - optional - default: true
## Instructs the check to validate the TLS certificate(s) of the service(s).
#
# tls_verify: true

## @param tls_ca_cert - string - optional
## The path to the `proxysql-ca.pem` file generated by ProxySQL when using SSL (or your own).
## More information: https://github.com/sysown/proxysql/wiki/SSL-Support#ssl-configuration-for-frontends
## The path to a file of concatenated CA certificates in PEM format or a directory
## containing several CA certificates in PEM format. If a directory, the directory
## must have been processed using the c_rehash utility supplied with OpenSSL. See:
## https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html
##
## Setting this implicitly sets `tls_verify` to true.
#
# tls_ca_cert: <CA_CERT_PATH>

## @param validate_hostname - boolean - optional - default: true
## Whether or not to verify the certificate was issued for the given `host`
## @param tls_cert - string - optional
## The path to a single file in PEM format containing a certificate as well as any
## number of CA certificates needed to establish the certificate's authenticity for
## use when connecting to services. It may also contain an unencrypted private key to use.
##
## Setting this implicitly sets `tls_verify` to true.
#
# tls_cert: <CERT_PATH>

## @param tls_private_key - string - optional
## The unencrypted private key to use for `tls_cert` when connecting to services. This is
## required if `tls_cert` is set and it does not already contain a private key.
##
## Setting this implicitly sets `tls_verify` to true.
#
# tls_private_key: <PRIVATE_KEY_PATH>

## @param tls_private_key_password - string - optional
## Optional password to decrypt tls_private_key.
##
## Setting this implicitly sets `tls_verify` to true.
#
# tls_private_key_password: <PRIVATE_KEY_PASSWORD>

## @param tls_validate_hostname - boolean - optional - default: true
## Verifies that the server's cert hostname matches the one requested.
#
# validate_hostname: true
# tls_validate_hostname: true

## @param connect_timeout - integer - optional - default: 10
## Timeout in seconds for connecting to ProxySQL.
Expand Down
30 changes: 20 additions & 10 deletions proxysql/datadog_checks/proxysql/proxysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pymysql
import pymysql.cursors

from datadog_checks.base import AgentCheck, ConfigurationError
from datadog_checks.base import AgentCheck, ConfigurationError, is_affirmative
from datadog_checks.base.utils.db import QueryManager

from .queries import (
Expand All @@ -18,7 +18,7 @@
STATS_MYSQL_USERS,
VERSION_METADATA,
)
from .ssl_utils import make_insecure_ssl_client_context, make_secure_ssl_client_context
from .ssl_utils import make_insecure_ssl_client_context

ADDITIONAL_METRICS_MAPPING = {
'command_counters_metrics': STATS_COMMAND_COUNTERS,
Expand All @@ -34,6 +34,11 @@ class ProxysqlCheck(AgentCheck):
SERVICE_CHECK_NAME = "can_connect"
__NAMESPACE__ = "proxysql"

# This remapper is used to support legacy Proxysql integration config values
TLS_CONFIG_REMAPPER = {
'validate_hostname': {'name': 'tls_validate_hostname'},
yzhan289 marked this conversation as resolved.
Show resolved Hide resolved
}

def __init__(self, name, init_config, instances):
super(ProxysqlCheck, self).__init__(name, init_config, instances)
self.host = self.instance.get("host", "")
Expand All @@ -44,9 +49,14 @@ def __init__(self, name, init_config, instances):
if not all((self.host, self.port, self.user, self.password)):
raise ConfigurationError("ProxySQL host, port, username and password are needed")

self.tls_verify = self.instance.get("tls_verify", False)
self.validate_hostname = self.instance.get("validate_hostname", True)
self.tls_ca_cert = self.instance.get("tls_ca_cert")
# If `tls_verify` is explicitly set to true, set `use_tls` to true (for legacy support)
# `tls_verify` used to do what `use_tls` does now
self.tls_verify = is_affirmative(self.instance.get('tls_verify'))
self.use_tls = is_affirmative(self.instance.get('use_tls', False))

if self.tls_verify and not self.use_tls:
self.use_tls = True

self.connect_timeout = self.instance.get("connect_timeout", 10)
self.read_timeout = self.instance.get("read_timeout")

Expand Down Expand Up @@ -90,13 +100,13 @@ def execute_query_raw(self, query):

@contextmanager
def connect(self):
if self.tls_verify:
if self.use_tls:
self.log.debug("Connecting to ProxySQL via SSL/TLS")
# If ca_cert is None, will load the default certificates
ssl_context = make_secure_ssl_client_context(
ca_cert=self.tls_ca_cert, check_hostname=self.validate_hostname
)
ssl_context = self.get_tls_context()
else:
ssl_context = make_insecure_ssl_client_context()
self.log.debug("Connecting to ProxySQL without SSL/TLS")
ssl_context = make_insecure_ssl_client_context() # can keep this
yzhan289 marked this conversation as resolved.
Show resolved Hide resolved

db = None
try:
Expand Down
53 changes: 0 additions & 53 deletions proxysql/datadog_checks/proxysql/ssl_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# (C) Datadog, Inc. 2020-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import os
import ssl

from datadog_checks.base import ConfigurationError


def make_insecure_ssl_client_context():
"""Creates an insecure ssl context for integration that requires to use TLS without checking
Expand All @@ -16,53 +13,3 @@ def make_insecure_ssl_client_context():
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_NONE
return context


def make_secure_ssl_client_context(
ca_cert=None, client_cert=None, client_key=None, check_hostname=True, protocol=ssl.PROTOCOL_TLS
):
"""Creates a secure ssl context for integration that requires one.
:param str ca_cert: Path to a file of concatenated CA certificates in PEM format or to a directory containing
several CA certificates in PEM format
:param str client_cert: Path to a single file in PEM format containing the certificate as well as any number of
CA certificates needed to establish the certificate's authenticity.
:param str client_key: Must point to a file containing the private key. Otherwise the private key will be taken
from certfile as well.
:param bool check_hostname: Whether to match the peer cert's hostname
:param int protocol: Client side protocol (should be one of the `ssl.PROTOCOL_*` constants)
By default selects the highest protocol version possible.

:rtype ssl.Context
"""
# https://docs.python.org/3/library/ssl.html#ssl.SSLContext
# https://docs.python.org/3/library/ssl.html#ssl.PROTOCOL_TLS
context = ssl.SSLContext(protocol=protocol)

# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.verify_mode
context.verify_mode = ssl.CERT_REQUIRED

# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.check_hostname
context.check_hostname = check_hostname

ca_file, ca_path = None, None
if os.path.isdir(ca_cert):
ca_path = ca_cert
elif os.path.isfile(ca_cert):
ca_file = ca_cert
else:
raise ConfigurationError("Specified tls_ca_cert: {} should be a valid file or directory.".format(ca_cert))

# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations
if ca_file or ca_path:
context.load_verify_locations(ca_file, ca_path, None)

# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_default_certs
else:
context.load_default_certs(ssl.Purpose.SERVER_AUTH)

# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain
if client_cert:
# If client_key is not defined, load_cert_chain reads the key from the client_cert
context.load_cert_chain(client_cert, keyfile=client_key)

return context
2 changes: 1 addition & 1 deletion proxysql/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def get_dependencies():
return f.readlines()


CHECKS_BASE_REQ = ['datadog-checks-base>=11.3.1'] # Needs fix integrations-core/#6146 for the QueryManager
CHECKS_BASE_REQ = ['datadog-checks-base>=15.2.0']


setup(
Expand Down
85 changes: 85 additions & 0 deletions proxysql/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# (C) Datadog, Inc. 2020-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import os

from datadog_checks.dev import get_docker_hostname

GLOBAL_METRICS = (
'proxysql.active_transactions',
'proxysql.query_processor_time_pct',
Expand Down Expand Up @@ -99,3 +103,84 @@
+ USER_TAGS_METRICS
+ QUERY_RULES_TAGS_METRICS
)

DOCKER_HOST = get_docker_hostname()
MYSQL_PORT = 6612
PROXY_PORT = 6033
PROXY_ADMIN_PORT = 6032
MYSQL_USER = 'proxysql'
MYSQL_PASS = 'pass'
PROXY_ADMIN_USER = 'proxy'
PROXY_ADMIN_PASS = 'proxy'
PROXY_STATS_USER = 'proxystats'
PROXY_STATS_PASS = 'proxystats'
MYSQL_DATABASE = 'test'
PROXY_MAIN_DATABASE = 'main'
PROXYSQL_VERSION = os.environ['PROXYSQL_VERSION']

BASIC_INSTANCE = {
'host': DOCKER_HOST,
'port': PROXY_ADMIN_PORT,
'username': PROXY_ADMIN_USER,
'password': PROXY_ADMIN_PASS,
'tags': ["application:test"],
'additional_metrics': [],
}


BASIC_INSTANCE_TLS = {
'host': DOCKER_HOST,
'port': PROXY_ADMIN_PORT,
'username': PROXY_ADMIN_USER,
'password': PROXY_ADMIN_PASS,
'tags': ["application:test"],
'additional_metrics': [],
'use_tls': True,
'tls_ca_cert': "/etc/ssl/certs/proxysql-ca.pem",
'tls_validate_hostname': True,
}


BASIC_INSTANCE_TLS_LEGACY = {
'host': DOCKER_HOST,
'port': PROXY_ADMIN_PORT,
'username': PROXY_ADMIN_USER,
'password': PROXY_ADMIN_PASS,
'tags': ["application:test"],
'additional_metrics': [],
'tls_verify': True, # legacy version of tls_verify
'tls_ca_cert': "/etc/ssl/certs/proxysql-ca.pem",
'validate_hostname': True, # legacy version of tls_validate_hostname
}


INSTANCE_ALL_METRICS = {
'host': DOCKER_HOST,
'port': PROXY_ADMIN_PORT,
'username': PROXY_ADMIN_USER,
'password': PROXY_ADMIN_PASS,
'tags': ["application:test"],
'additional_metrics': [
'command_counters_metrics',
'connection_pool_metrics',
'users_metrics',
'memory_metrics',
'query_rules_metrics',
],
}

INSTANCE_ALL_METRICS_STATS = {
'host': DOCKER_HOST,
'port': PROXY_ADMIN_PORT,
'username': PROXY_STATS_USER,
'password': PROXY_STATS_PASS,
'database_name': PROXY_MAIN_DATABASE,
'tags': ["application:test"],
'additional_metrics': [
'command_counters_metrics',
'connection_pool_metrics',
'users_metrics',
'memory_metrics',
'query_rules_metrics',
],
}
Loading