From 70f565c5037eba91e5dcfe50d4a21b9200eed8ae Mon Sep 17 00:00:00 2001 From: Gabriel Filion Date: Sun, 19 Jan 2020 19:40:29 -0500 Subject: [PATCH] puppetdb: implement cert validation and client cert auth It is very common for PuppetDB installs to run on a different host than the puppetmaster. In such cases, a certificate file is normally used to establish an encrypted communication to the server. The most common setup for this server certificate is to use a certificate that was signed by the puppetmaster's CA, so one would want to verify the server cert against this same CA (it should be present on puppet clients). Moreover, to ensure that *only* puppet clients can communicate with PuppetDB, a pair of client certificat/key files are usually used to authenticate clients. --- plugins/puppet/puppetdb | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/plugins/puppet/puppetdb b/plugins/puppet/puppetdb index 7eb1352a..a4d55aa4 100755 --- a/plugins/puppet/puppetdb +++ b/plugins/puppet/puppetdb @@ -21,11 +21,33 @@ Plugin configuration parameters: Time in seconds (int) to wait for a result when querying the REST API. By default, wait for 2 seconds + * ca: + Path to the Certificate Authority used for verifying a cert received from + the PuppetDB server during an https connection. This can be useful if the + cert used by PuppetDB was signed by the puppetmaster's CA. This option is + not necessary if a plaintext connection is used (e.g. if pdburl starts + with 'http://'). + + * cert: + Path to the TLS certificate file used for establishing client + communication for an https connection. This option should be paired with + the `key` option. This option is not necessary if a plaintext connection + is used (e.g. if pdburl starts with 'http://'). + + * key: + Path to the TLS private key used for establishing client communication for + an https connection. This option should be paired with the `cert` option. + This option is not necessary if a plaintext connection is used (e.g. if + pdburl starts with 'http://'). + Example: [puppetdb] env.pdburl https://puppetdb.example.com:8080/metrics/v1/mbeans env.timeout 5 + env.ca /etc/puppetboard/ca.pem + env.cert /etc/puppetboard/client_cert.pem + env.key /etc/puppetboard/client_key.pem =head1 DEPENDENCIES @@ -54,14 +76,21 @@ class WrongStatusCode(Exception): pass -def rest_request(url, timeout): +def rest_request(url, timeout, ca, key_pair): """Make a GET request to URL. We expect a 200 response. This function will let exceptions from requests raise through to indicate request failure. If response code is not 200, it will raise a WrongStatusCode exception. """ headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} - resp = requests.get(url, headers=headers, timeout=timeout) + + ssl_options = {} + if ca: + ssl_options['verify'] = ca + if key_pair: + ssl_options['cert'] = key_pair + + resp = requests.get(url, headers=headers, timeout=timeout, **ssl_options) if resp.status_code != 200: err = f"GET Request to '{url}' returned code {resp.status_code}; expected 200." # noqa: E501 raise WrongStatusCode(err) @@ -93,12 +122,12 @@ def config(): print("jvm_mem_used.draw AREA") -def fetch_field_values(mbeans_url, timeout): +def fetch_field_values(mbeans_url, timeout, ca, key_pair): """Get values from PuppetDB and print them out.""" memory_url = f"{mbeans_url}/java.lang:type=Memory" try: - mem_req = rest_request(memory_url, timeout) + mem_req = rest_request(memory_url, timeout, ca, key_pair) except Exception as e: print(f"HTTP Request did not complete successfully: {e}", file=sys.stderr) @@ -134,9 +163,31 @@ if __name__ == '__main__': print(f"Invalid value for timeout: {e}", file=sys.stderr) exit(1) + ca = os.environ.get('ca', None) + if ca: + if not os.path.exists(ca): + print(f"CA file '{ca}' not found.", file=sys.stderr) + exit(1) + + cert = os.environ.get('cert', None) + key = os.environ.get('key', None) + if cert or key: + if cert and key: + if not os.path.exists(cert): + print(f"Certificate file '{cert}' not found.", file=sys.stderr) + exit(1) + if not os.path.exists(key): + print(f"Key file '{key}' not found.", file=sys.stderr) + exit(1) + else: + print("Only one of 'cert' and 'key' supplied. " + "Both are needed for client authentication.", + file=sys.stderr) + exit(1) + if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': try: - dummy = rest_request(mbeans_url, timeout) + dummy = rest_request(mbeans_url, timeout, ca, (cert, key)) except Exception as e: print(f"no ({e})") exit(0) @@ -148,4 +199,4 @@ if __name__ == '__main__': config() exit(0) - fetch_field_values(mbeans_url, timeout) + fetch_field_values(mbeans_url, timeout, ca, (cert, key))