Skip to content

Commit

Permalink
Merge pull request #9 from Conjur-Enterprise/enhancement-request
Browse files Browse the repository at this point in the history
CNJR-6939 updated the lookup plugin and tests to support certificate content
  • Loading branch information
Brugu Maharishi authored and GitHub Enterprise committed Dec 19, 2024
2 parents d31e892 + 9e4ffe2 commit 8770992
Show file tree
Hide file tree
Showing 33 changed files with 688 additions and 33 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,45 @@ Conjur host, if they are present on the system running the lookup plugin.
- `CONJUR_ACCOUNT` : The Conjur account name
- `CONJUR_APPLIANCE_URL` : URL of the running Conjur service
- `CONJUR_CERT_FILE` : Path to the Conjur certificate file
- `CONJUR_CERT_CONTENT` : Content of the Conjur certificate (PEM format).
- `CONJUR_AUTHN_LOGIN` : A valid Conjur host username
- `CONJUR_AUTHN_API_KEY` : The api key that corresponds to the Conjur host username
- `CONJUR_AUTHN_TOKEN_FILE` : Path to a file containing a valid Conjur auth token

### Certificate Content Format

In addition to specifying a certificate file (CONJUR_CERT_FILE), you can now provide the certificate content directly via the CONJUR_CERT_CONTENT variable. This is useful when you prefer to include the certificate in the form of a string (PEM format) instead of referencing a file on disk.

### How it works

The lookup plugin will first attempt to use the CONJUR_CERT_CONTENT variable. If it is invalid or missing, the plugin will fall back to using the certificate file specified in the CONJUR_CERT_FILE.

### Example

1. CONJUR_CERT_CONTENT as PEM format

You can provide the certificate directly as a string in PEM format inside the CONJUR_CERT_CONTENT environment variable. Ensure that the content includes the full certificate block starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----.

```sh
export CONJUR_CERT_CONTENT="-----BEGIN CERTIFICATE-----
your certificate content
-----END CERTIFICATE-----"
```

Once set, the plugin will look for this variable and use the certificate content as needed.

2. CONJUR_CERT_FILE as File

If CONJUR_CERT_CONTENT is not set or is invalid, the plugin will attempt to use the certificate file specified in the CONJUR_CERT_FILE environment variable.

To specify the certificate file, set the environment variable like this:

```sh
export CONJUR_CERT_FILE="<path>/certificate.pem"
```

3. If both CONJUR_CERT_CONTENT and CONJUR_CERT_FILE are missing or invalid, the plugin will return an error. This allows you to quickly diagnose the issue and ensure that the required certificate is provided.

### Role Variables

None.
Expand Down
150 changes: 126 additions & 24 deletions plugins/lookup/conjur_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
description:
Retrieves credentials from Conjur using the controlling host's Conjur identity,
environment variables, or extra-vars.
Environment variables could be CONJUR_ACCOUNT, CONJUR_APPLIANCE_URL, CONJUR_CERT_FILE, CONJUR_AUTHN_LOGIN, CONJUR_AUTHN_API_KEY, CONJUR_AUTHN_TOKEN_FILE
Extra-vars could be conjur_account, conjur_appliance_url, conjur_cert_file, conjur_authn_login, conjur_authn_api_key, conjur_authn_token_file
Environment variables could be CONJUR_ACCOUNT, CONJUR_APPLIANCE_URL, CONJUR_CERT_FILE, CONJUR_CERT_CONTENT,
CONJUR_AUTHN_LOGIN, CONJUR_AUTHN_API_KEY, CONJUR_AUTHN_TOKEN_FILE
Extra-vars could be conjur_account, conjur_appliance_url, conjur_cert_file, conjur_cert_content,
conjur_authn_login, conjur_authn_api_key, conjur_authn_token_file
Conjur info - U(https://www.conjur.org/).
requirements:
- 'The controlling host running Ansible has a Conjur identity.
Expand Down Expand Up @@ -112,6 +114,17 @@
- name: conjur_cert_file
env:
- name: CONJUR_CERT_FILE
conjur_cert_content:
description: Content of the Conjur cert
type: string
required: False
ini:
- section: conjur,
key: cert_content
vars:
- name: conjur_cert_content
env:
- name: CONJUR_CERT_CONTENT
conjur_authn_token_file:
description: Path to the access token file
type: path
Expand Down Expand Up @@ -142,8 +155,12 @@
- Value stored in Conjur.
"""

import os.path
import os
import socket
import yaml
import traceback
import ssl
import re
import ansible.module_utils.six.moves.urllib.error as urllib_error
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
Expand All @@ -153,12 +170,85 @@
from ansible.module_utils.six.moves.urllib.parse import quote
from stat import S_IRUSR, S_IWUSR
from tempfile import gettempdir, NamedTemporaryFile
import yaml
try:
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.backends import default_backend
except ImportError:
CRYPTOGRAPHY_IMPORT_ERROR = traceback.format_exc()
else:
CRYPTOGRAPHY_IMPORT_ERROR = None


from ansible.module_utils.urls import open_url
from ansible.utils.display import Display

display = Display()
temp_cert_file = None


def _validate_pem_certificate(cert_content):
# Normalize line endings
if '\r\n' in cert_content:
cert_content = cert_content.replace('\r\n', '\n').strip()
elif '\r' in cert_content:
cert_content = cert_content.replace('\r', '\n').strip()
cert_content = re.sub(r'^[ \t]+', '', cert_content, flags=re.M)
cert_content = re.sub(r'[ \t]+$', '', cert_content, flags=re.M)
cert_content = re.sub(r'\n+', '\n', cert_content)

if not re.match(r"^-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----$", cert_content, re.DOTALL):
raise AnsibleError("Invalid Certificate format.")

try:
load_pem_x509_certificate(cert_content.encode(), default_backend())
return cert_content
except ValueError as e:
raise AnsibleError(f"Invalid certificate content provided: {str(e)}. Please check the certificate format.")
except ssl.SSLError as e:
raise AnsibleError(f"SSL error while validating the certificate: {str(e)}. The certificate may be corrupted or invalid.")
except Exception as e:
raise AnsibleError(f"An error occurred while validating the certificate: {str(e)}. Please verify the certificate format and try again.")


def _get_valid_certificate(cert_content, cert_file):
if cert_content:
try:
display.vvv("Validating provided certificate content")
cert_content = _validate_pem_certificate(cert_content)
return cert_content
except AnsibleError as e:
display.warning(f"Invalid certificate content: {str(e)}. Attempting to use certificate file.")

# If cert_content is invalid or missing, fall back to cert_file
if cert_file:
if not os.path.exists(cert_file):
raise AnsibleError(f"Certificate file `{cert_file}` does not exist or cannot be found.")
try:
with open(cert_file, 'rb') as f:
cert_file_content = f.read().decode('utf-8')
cert_file_content = _validate_pem_certificate(cert_file_content)
return cert_file_content
except Exception as e:
raise AnsibleError(f"Failed to load or validate certificate file `{cert_file}`: {str(e)}")

# If both cert_content and cert_file are missing or invalid, raise an error
raise AnsibleError("Both certificate content and certificate file are invalid or missing. Please provide a valid certificate.")


def _get_certificate_file(cert_content, cert_file):
global temp_cert_file
cert_content = _get_valid_certificate(cert_content, cert_file)

if cert_content:
try:
temp_cert_file = NamedTemporaryFile(delete=False, mode='w', encoding='utf-8')
temp_cert_file.write(cert_content)
temp_cert_file.close()
cert_file = temp_cert_file.name
except Exception as e:
raise AnsibleError(f"Failed to create temporary certificate file: {str(e)}")

return cert_file


# Load configuration and return as dictionary if file is present on file system
Expand Down Expand Up @@ -345,6 +435,7 @@ def run(self, terms, variables=None, **kwargs):
authn_login = self.get_var_value("conjur_authn_login")
authn_api_key = self.get_var_value("conjur_authn_api_key")
cert_file = self.get_var_value("conjur_cert_file")
cert_content = self.get_var_value("conjur_cert_content")
authn_token_file = self.get_var_value("conjur_authn_token_file")

validate_certs = self.get_option('validate_certs')
Expand All @@ -357,6 +448,9 @@ def run(self, terms, variables=None, **kwargs):
if 'http://' in str(appliance_url):
raise AnsibleError(('[WARNING]: Conjur URL uses insecure connection. Please consider using HTTPS.'))

if validate_certs is True:
cert_file = _get_certificate_file(cert_content, cert_file)

conf = _merge_dictionaries(
_load_conf_from_file(conf_file),
{
Expand Down Expand Up @@ -414,31 +508,39 @@ def run(self, terms, variables=None, **kwargs):
display.vvv("Using cert file path {0}".format(conf['cert_file']))
cert_file = conf['cert_file']

token = None
if 'authn_token_file' not in conf:
token = _fetch_conjur_token(
try:
token = None
if 'authn_token_file' not in conf:
token = _fetch_conjur_token(
conf['appliance_url'],
conf['account'],
identity['id'],
identity['api_key'],
validate_certs,
cert_file
)
else:
if not os.path.exists(conf['authn_token_file']):
raise AnsibleError('Conjur authn token file `{0}` was not found on the host'
.format(conf['authn_token_file']))
with open(conf['authn_token_file'], 'rb') as f:
token = f.read()

conjur_variable = _fetch_conjur_variable(
terms[0],
token,
conf['appliance_url'],
conf['account'],
identity['id'],
identity['api_key'],
validate_certs,
cert_file
)
else:
if not os.path.exists(conf['authn_token_file']):
raise AnsibleError('Conjur authn token file `{0}` was not found on the host'
.format(conf['authn_token_file']))
with open(conf['authn_token_file'], 'rb') as f:
token = f.read()

conjur_variable = _fetch_conjur_variable(
terms[0],
token,
conf['appliance_url'],
conf['account'],
validate_certs,
cert_file
)
finally:
if temp_cert_file:
try:
if os.path.exists(temp_cert_file.name):
os.unlink(temp_cert_file.name)
except (OSError, PermissionError) as e:
raise AnsibleError(f"Failed to delete temporary certificate file `{temp_cert_file.name}`: {str(e)}")

if as_file:
return _store_secret_in_file(conjur_variable)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export CONJUR_CERT_CONTENT="MIBIADERGYHHFV5ANBgkqhkiG9w0BAQEFAAerdfhBCgKCAQEA..."
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
- name: Retrieve Conjur variable fails with bad cert content
hosts: localhost
connection: local
tasks:
- name: Clean artifact path
file:
state: absent
path: /conjur_secrets.txt

- name: Determine lookup path based on CONJUR flavour
set_fact:
lookup_path: >-
{{
'data/ansible' if lookup('env', 'IS_CLOUD') | bool else 'ansible'
}}
- name: Retrieve Conjur variable with bad cert content
vars:
super_secret_key: "{{ lookup('conjur_variable', lookup_path + '/test-secret') }}"
shell: echo "{{super_secret_key}}" > /conjur_secrets.txt
ignore_errors: True
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os

testinfra_hosts = [os.environ['COMPOSE_PROJECT_NAME'] + '-ansible-1']


def test_retrieval_failed(host):
secrets_file = host.file('/conjur_secrets.txt')

assert not secrets_file.exists
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export CONJUR_CERT_FILE=""
export CONJUR_CERT_CONTENT=""
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export CONJUR_CERT_CONTENT=$(cat /cyberark/dev/conjur.pem | sed 's/$/\r/')
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
- name: Retrieve Conjur variable
hosts: localhost
connection: local
tasks:
- name: Clean artifact path
file:
state: absent
path: /conjur_secrets.txt

- name: Determine lookup path based on CONJUR flavour
set_fact:
lookup_path: >-
{{
'data/ansible' if lookup('env', 'IS_CLOUD') | bool else 'ansible'
}}
- name: Retrieve Conjur variable
vars:
super_secret_key: "{{ lookup('conjur_variable', lookup_path + '/test-secret') }}"
shell: echo "{{super_secret_key}}" > /conjur_secrets.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os


testinfra_hosts = [os.environ['COMPOSE_PROJECT_NAME'] + '-ansible-1']


def test_retrieved_secret(host):
secrets_file = host.file('/conjur_secrets.txt')

assert secrets_file.exists

result = host.check_output("cat /conjur_secrets.txt", shell=True)

assert result == "test_secret_password"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export CONJUR_CERT_CONTENT=$(cat /cyberark/dev/conjur.pem | sed 's/$/\r\n/')
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
- name: Retrieve Conjur variable
hosts: localhost
connection: local
tasks:
- name: Clean artifact path
file:
state: absent
path: /conjur_secrets.txt

- name: Determine lookup path based on CONJUR flavour
set_fact:
lookup_path: >-
{{
'data/ansible' if lookup('env', 'IS_CLOUD') | bool else 'ansible'
}}
- name: Retrieve Conjur variable
vars:
super_secret_key: "{{ lookup('conjur_variable', lookup_path + '/test-secret') }}"
shell: echo "{{super_secret_key}}" > /conjur_secrets.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os


testinfra_hosts = [os.environ['COMPOSE_PROJECT_NAME'] + '-ansible-1']


def test_retrieved_secret(host):
secrets_file = host.file('/conjur_secrets.txt')

assert secrets_file.exists

result = host.check_output("cat /conjur_secrets.txt", shell=True)

assert result == "test_secret_password"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
unset CONJUR_AUTHN_API_KEY
unset CONJUR_AUTHN_LOGIN
export CONJUR_AUTHN_TOKEN_FILE=/cyberark/dev/access_token
export CONJUR_CERT_CONTENT="MIBIADERGYHHFV5ANBgkqhkiG9w0BAQEFAAerdfhBCgKCAQEA..."
Loading

0 comments on commit 8770992

Please sign in to comment.