TLS Certificates Interface
- Canonical Telco
Channel | Revision | Published | Runs on |
---|---|---|---|
latest/edge | 130 | 12 Nov 2024 | |
latest/edge | 19 | 10 Feb 2023 |
juju deploy tls-certificates-interface --channel edge
Deploy Kubernetes operators easily with Juju, the Universal Operator Lifecycle Manager. Need a Kubernetes cluster? Install MicroK8s to create a full CNCF-certified Kubernetes system in under 60 seconds.
Platform:
Getting Started (v4)
In this tutorial, we will take a working Nginx charm and add the TLS Certificates integration using the TLS certificates library v4.
Pre-requisites
Knowledge
- You have some experience writing charms and are comfortable with the charm development tooling
Software
- Ubuntu 22.04
- A Juju controller running on a MicroK8s
- Charmcraft
1. Pack and deploy the nginx demo charm
In this section, we will deploy a demo Nginx charm and access its HTTP address via our browser.
Clone the TLS Certificates Interface Demo project:
git clone git@github.com:canonical/tls-certificates-interface-demo.git
Move to the repo directory:
cd tls-certificates-interface-demo
Pack the charm
charmcraft pack
Create a Juju model:
juju add-model demo
Deploy the charm
juju deploy ./tls-certificates-interface-demo_ubuntu-22.04-amd64.charm nginx-http --resource nginx-image=nginx:1.27.1
Wait for the charm to go to the Active/Idle status:
guillaume@thinkpad:~/code/tls-certificates-interface-demo$ juju status
Model Controller Cloud/Region Version SLA Timestamp
demo microk8s-localhost microk8s/localhost 3.5.3 unsupported 15:12:25-04:00
App Version Status Scale Charm Channel Rev Address Exposed Message
nginx-http active 1 tls-certificates-interface-demo 0 10.152.183.199 no
Unit Workload Agent Address Ports Message
nginx-http/0* active idle 10.1.19.158
Using your browser, navigate to the application address on port 8080 using the HTTP scheme. Here this would be http://10.152.183.199:8080
.
You should see the following page:
We now know that have a working Nginx charm.
2. Import and use the TLS Certificates Library
This section outlines the changes that have to be made to the Nginx charm to support the TLS certificates integration. You can use this pull request as reference as well.
Fetch the TLS Certificates charm library
charmcraft fetch-lib charms.tls_certificates_interface.v4.tls_certificates
Add the following packages to the charm’s requirements.txt
file:
cryptography==43.0.1
pydantic==2.9.1
Add the following section to the charm’s charmcraft.yaml
file:
requires:
certificates:
interface: tls-certificates
parts:
charm:
build-packages:
- cargo
- libffi-dev
- libssl-dev
- pkg-config
- rustc
All of the following changes here are made in src/charm.py
.
Import the following classes from TLS Certificates Interface Library in your charm code:
from lib.charms.tls_certificates_interface.v4.tls_certificates import (
Certificate,
CertificateRequestAttributes,
Mode,
PrivateKey,
TLSCertificatesRequiresV4,
)
Add the following global variables:
CERTS_DIR_PATH = "/etc/nginx"
PRIVATE_KEY_NAME = "nginx.key"
CERTIFICATE_NAME = "nginx.pem"
In the charm’s class constructor, instantiate a TLSCertificatesRequiresV4
object and have the Certificate Available
Event handled by our central configure
event handler:
class TlsCertificatesInterfaceDemoCharm(ops.CharmBase):
"""Charm the service."""
def __init__(self, framework: ops.Framework):
super().__init__(framework)
...
self.certificates = TLSCertificatesRequiresV4(
charm=self,
relationship_name="certificates",
certificate_requests=[self._get_certificate_request_attributes()],
mode=Mode.UNIT,
)
...
framework.observe(
self.certificates.on.certificate_available, self._configure
)
...
Add a _get_certificate_request_attributes
method that returns the list of certificate requested by the charm:
class TlsCertificatesInterfaceDemoCharm(ops.CharmBase):
def _get_certificate_request_attributes(self) -> CertificateRequestAttributes:
return CertificateRequestAttributes(common_name="example.com")
In the collect unit status event handler, have the charm go to Blocked
status until it is integrated with a TLS Certificates Provider:
class TlsCertificatesInterfaceDemoCharm(ops.CharmBase):
def __init__(self, framework: ops.Framework):
...
framework.observe(self.on.collect_unit_status, self._on_collect_status)
def _on_collect_status(self, event: ops.CollectStatusEvent):
...
if not self._relation_created("certificates"):
event.add_status(
ops.BlockedStatus("certificates integration not created")
)
return
...
Update the _configure
event handler to manage TLS Certificates and restart the Pebble container when certificates have changed:
class TlsCertificatesInterfaceDemoCharm(ops.CharmBase):
"""Charm the service."""
def __init__(self, framework: ops.Framework):
super().__init__(framework)
...
framework.observe(self.on["nginx"].pebble_ready, self._configure)
framework.observe(self.on.config_changed, self._configure)
framework.observe(self.certificates.on.certificate_available, self._configure)
def _configure(self, _: ops.EventBase):
if not self.container.can_connect():
return
if not self._relation_created("certificates"):
return
if not self._certificate_is_available():
return
certificate_update_required = self._check_and_update_certificate()
desired_config_file = self._generate_config_file()
if config_update_required := self._is_config_update_required(desired_config_file):
self._push_config_file(content=desired_config_file)
should_restart = config_update_required or certificate_update_required
self._configure_pebble(restart=should_restart)
We will need the following methods to handle pulling, pushing and comparing certificates:
class TlsCertificatesInterfaceDemoCharm(ops.CharmBase):
def _relation_created(self, relation_name: str) -> bool:
return bool(self.model.relations.get(relation_name))
def _certificate_is_available(self) -> bool:
cert, key = self.certificates.get_assigned_certificate(
certificate_request=self._get_certificate_request_attributes()
)
return bool(cert and key)
def _check_and_update_certificate(self) -> bool:
"""Check if the certificate or private key needs an update and perform the update.
This method retrieves the currently assigned certificate and private key associated with
the charm's TLS relation. It checks whether the certificate or private key has changed
or needs to be updated. If an update is necessary, the new certificate or private key is
stored.
Returns:
bool: True if either the certificate or the private key was updated, False otherwise.
"""
provider_certificate, private_key = self.certificates.get_assigned_certificate(
certificate_request=self._get_certificate_request_attributes()
)
if not provider_certificate or not private_key:
logger.debug("Certificate or private key is not available")
return False
if certificate_update_required := self._is_certificate_update_required(
provider_certificate.certificate
):
self._store_certificate(certificate=provider_certificate.certificate)
if private_key_update_required := self._is_private_key_update_required(private_key):
self._store_private_key(private_key=private_key)
return certificate_update_required or private_key_update_required
def _is_certificate_update_required(self, certificate: Certificate) -> bool:
return self._get_existing_certificate() != certificate
def _is_private_key_update_required(self, private_key: PrivateKey) -> bool:
return self._get_existing_private_key() != private_key
def _get_existing_certificate(self) -> Optional[Certificate]:
return self._get_stored_certificate() if self._certificate_is_stored() else None
def _get_existing_private_key(self) -> Optional[PrivateKey]:
return self._get_stored_private_key() if self._private_key_is_stored() else None
def _certificate_is_stored(self) -> bool:
return self.container.exists(path=f"{CERTS_DIR_PATH}/{CERTIFICATE_NAME}")
def _private_key_is_stored(self) -> bool:
return self.container.exists(path=f"{CERTS_DIR_PATH}/{PRIVATE_KEY_NAME}")
def _get_stored_certificate(self) -> Certificate:
cert_string = str(self.container.pull(path=f"{CERTS_DIR_PATH}/{CERTIFICATE_NAME}").read())
return Certificate.from_string(cert_string)
def _get_stored_private_key(self) -> PrivateKey:
key_string = str(self.container.pull(path=f"{CERTS_DIR_PATH}/{PRIVATE_KEY_NAME}").read())
return PrivateKey.from_string(key_string)
def _store_certificate(self, certificate: Certificate) -> None:
"""Store certificate in workload."""
self.container.push(path=f"{CERTS_DIR_PATH}/{CERTIFICATE_NAME}", source=str(certificate))
logger.info("Pushed certificate pushed to workload")
def _store_private_key(self, private_key: PrivateKey) -> None:
"""Store private key in workload."""
self.container.push(
path=f"{CERTS_DIR_PATH}/{PRIVATE_KEY_NAME}",
source=str(private_key),
)
logger.info("Pushed private key to workload")
3. Deploy the nginx charm and integrate it with a TLS provider
Deploy the new nginx charm:
juju deploy ./tls-certificates-interface-demo_ubuntu-22.04-amd64.charm nginx-https --resource nginx-image=nginx:1.27.1
Wait for the charm to go to the Blocked/idle status:
guillaume@thinkpad:~/code/tls-certificates-interface-demo$ juju status
Model Controller Cloud/Region Version SLA Timestamp
demo microk8s-localhost microk8s/localhost 3.5.3 unsupported 15:15:22-04:00
App Version Status Scale Charm Channel Rev Address Exposed Message
nginx-http active 1 tls-certificates-interface-demo 0 10.152.183.199 no
nginx-https blocked 1 tls-certificates-interface-demo 1 10.152.183.188 no certificates integration not created
Unit Workload Agent Address Ports Message
nginx-http/0* active idle 10.1.19.158
nginx-https/0* blocked idle 10.1.19.145 certificates integration not created
Deploy Self Signed Certificates (a TLS Certificates provider), and integrate it with the nginx charm
juju deploy self-signed-certificates
juju integrate self-signed-certificates:certificates nginx-https:certificates
Wait for the nginx-https
charm to go to the Active/Idle status:
guillaume@thinkpad:~/code/tls-certificates-interface-demo$ juju status
Model Controller Cloud/Region Version SLA Timestamp
demo microk8s-localhost microk8s/localhost 3.5.3 unsupported 15:17:13-04:00
App Version Status Scale Charm Channel Rev Address Exposed Message
nginx-http active 1 tls-certificates-interface-demo 0 10.152.183.199 no
nginx-https waiting 1 tls-certificates-interface-demo 1 10.152.183.188 no installing agent
self-signed-certificates active 1 self-signed-certificates latest/stable 155 10.152.183.242 no
Unit Workload Agent Address Ports Message
nginx-http/0* active idle 10.1.19.158
nginx-https/0* active idle 10.1.19.145
self-signed-certificates/0* active idle 10.1.19.146
Using your browser, navigate to the application address on port 8080 using the HTTPS scheme. Here this would be https://10.152.183.188:8080
.
You should see the following page telling you that the HTTPs certificate is not valid:
Don’t worry, this message is expected since the certificate we received is self-signed.
Click on Advanced → Proceed and you should now see the same Nginx page as in step 1.
You can inspect the certificate and notice that you received a certificate for example.com
.
Congratulations, you added the TLS integration to your charm.