Skip to content

Commit 1d05178

Browse files
Fix SANs list (canonical#58)
* Fix SANs list * Improve TLS lib * Add new exception [skip ci] * Pin juju version * Improve library property set
1 parent 9eb9730 commit 1d05178

File tree

5 files changed

+69
-28
lines changed

5 files changed

+69
-28
lines changed

lib/charms/postgresql_k8s/v0/postgresql_tls.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""
1919

2020
import base64
21+
import ipaddress
2122
import logging
2223
import re
2324
import socket
@@ -44,7 +45,7 @@
4445

4546
# Increment this PATCH version before using `charmcraft publish-lib` or reset
4647
# to 0 if you are raising the major API version.
47-
LIBPATCH = 3
48+
LIBPATCH = 4
4849

4950
logger = logging.getLogger(__name__)
5051
SCOPE = "unit"
@@ -54,11 +55,14 @@
5455
class PostgreSQLTLS(Object):
5556
"""In this class we manage certificates relation."""
5657

57-
def __init__(self, charm, peer_relation):
58+
def __init__(
59+
self, charm, peer_relation: str, additional_dns_names: Optional[List[str]] = None
60+
):
5861
"""Manager of PostgreSQL relation with TLS Certificates Operator."""
5962
super().__init__(charm, "client-relations")
6063
self.charm = charm
6164
self.peer_relation = peer_relation
65+
self.additional_dns_names = additional_dns_names or []
6266
self.certs = TLSCertificatesRequiresV1(self.charm, TLS_RELATION)
6367
self.framework.observe(
6468
self.charm.on.set_tls_private_key_action, self._on_set_tls_private_key
@@ -86,8 +90,8 @@ def _request_certificate(self, param: Optional[str]):
8690
csr = generate_csr(
8791
private_key=key,
8892
subject=self.charm.get_hostname_by_unit(self.charm.unit.name),
89-
sans=self._get_sans(),
9093
additional_critical_extensions=self._get_tls_extensions(),
94+
**self._get_sans(),
9195
)
9296

9397
self.charm.set_secret(SCOPE, "key", key.decode("utf-8"))
@@ -149,28 +153,49 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
149153
new_csr = generate_csr(
150154
private_key=key,
151155
subject=self.charm.get_hostname_by_unit(self.charm.unit.name),
152-
sans=self._get_sans(),
153156
additional_critical_extensions=self._get_tls_extensions(),
157+
**self._get_sans(),
154158
)
155159
self.certs.request_certificate_renewal(
156160
old_certificate_signing_request=old_csr,
157161
new_certificate_signing_request=new_csr,
158162
)
159163
self.charm.set_secret(SCOPE, "csr", new_csr.decode("utf-8"))
160164

161-
def _get_sans(self) -> List[str]:
162-
"""Create a list of DNS names for a PostgreSQL unit.
165+
def _get_sans(self) -> dict:
166+
"""Create a list of Subject Alternative Names for a PostgreSQL unit.
163167
164168
Returns:
165-
A list representing the hostnames of the PostgreSQL unit.
169+
A list representing the IP and hostnames of the PostgreSQL unit.
166170
"""
171+
172+
def is_ip_address(address: str) -> bool:
173+
"""Returns whether and address is an IP address."""
174+
try:
175+
ipaddress.ip_address(address)
176+
return True
177+
except (ipaddress.AddressValueError, ValueError):
178+
return False
179+
167180
unit_id = self.charm.unit.name.split("/")[1]
168-
return [
181+
182+
# Create a list of all the Subject Alternative Names.
183+
sans = [
169184
f"{self.charm.app.name}-{unit_id}",
170185
self.charm.get_hostname_by_unit(self.charm.unit.name),
171186
socket.getfqdn(),
172187
str(self.charm.model.get_binding(self.peer_relation).network.bind_address),
173188
]
189+
sans.extend(self.additional_dns_names)
190+
191+
# Separate IP addresses and DNS names.
192+
sans_ip = [san for san in sans if is_ip_address(san)]
193+
sans_dns = [san for san in sans if not is_ip_address(san)]
194+
195+
return {
196+
"sans_ip": sans_ip,
197+
"sans_dns": sans_dns,
198+
}
174199

175200
@staticmethod
176201
def _get_tls_extensions() -> Optional[List[ExtensionType]]:

lib/charms/tls_certificates_interface/v1/tls_certificates.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
222222
import logging
223223
import uuid
224224
from datetime import datetime, timedelta
225+
from ipaddress import IPv4Address
225226
from typing import Dict, List, Optional
226227

227228
from cryptography import x509
@@ -657,7 +658,9 @@ def generate_csr(
657658
email_address: str = None,
658659
country_name: str = None,
659660
private_key_password: Optional[bytes] = None,
660-
sans: Optional[List[str]] = None,
661+
sans_oid: Optional[str] = None,
662+
sans_ip: Optional[List[str]] = None,
663+
sans_dns: Optional[List[str]] = None,
661664
additional_critical_extensions: Optional[List] = None,
662665
) -> bytes:
663666
"""Generates a CSR using private key and subject.
@@ -672,7 +675,9 @@ def generate_csr(
672675
email_address (str): Email address.
673676
country_name (str): Country Name.
674677
private_key_password (bytes): Private key password
675-
sans (list): List of subject alternative names
678+
sans_dns (list): List of DNS subject alternative names
679+
sans_ip (list): List of IP subject alternative names
680+
sans_oid (str): Additional OID
676681
additional_critical_extensions (list): List if critical additional extension objects.
677682
Object must be a x509 ExtensionType.
678683
@@ -693,10 +698,17 @@ def generate_csr(
693698
if country_name:
694699
subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
695700
csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
696-
if sans:
697-
csr = csr.add_extension(
698-
x509.SubjectAlternativeName([x509.DNSName(san) for san in sans]), critical=False
699-
)
701+
702+
_sans = []
703+
if sans_oid:
704+
_sans.append(x509.RegisteredID(x509.ObjectIdentifier(sans_oid)))
705+
if sans_ip:
706+
_sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
707+
if sans_dns:
708+
_sans.extend([x509.DNSName(san) for san in sans_dns])
709+
if _sans:
710+
csr = csr.add_extension(x509.SubjectAlternativeName(_sans), critical=False)
711+
700712
if additional_critical_extensions:
701713
for extension in additional_critical_extensions:
702714
csr = csr.add_extension(extension, critical=True)

src/charm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def __init__(self, *args):
8787
self.postgresql_client_relation = PostgreSQLProvider(self)
8888
self.legacy_db_relation = DbProvides(self, admin=False)
8989
self.legacy_db_admin_relation = DbProvides(self, admin=True)
90-
self.tls = PostgreSQLTLS(self, PEER)
90+
self.tls = PostgreSQLTLS(self, PEER, [self.primary_endpoint, self.replicas_endpoint])
9191
self.restart_manager = RollingOpsManager(
9292
charm=self, relation="restart", callback=self._restart
9393
)

tests/unit/test_postgresql_tls.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,16 @@ def test_get_sans(self):
187187
sans = self.charm.tls._get_sans()
188188
self.assertEqual(
189189
sans,
190-
[
191-
"postgresql-k8s-0",
192-
"postgresql-k8s-0.postgresql-k8s-endpoints",
193-
socket.getfqdn(),
194-
"1.1.1.1",
195-
],
190+
{
191+
"sans_ip": ["1.1.1.1"],
192+
"sans_dns": [
193+
"postgresql-k8s-0",
194+
"postgresql-k8s-0.postgresql-k8s-endpoints",
195+
socket.getfqdn(),
196+
"postgresql-k8s-primary.None.svc.cluster.local",
197+
"postgresql-k8s-replicas.None.svc.cluster.local",
198+
],
199+
},
196200
)
197201

198202
def test_get_tls_extensions(self):

tox.ini

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ commands =
6868
[testenv:charm-integration]
6969
description = Run charm integration tests
7070
deps =
71-
juju
71+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
7272
pytest
7373
pytest-operator
7474
psycopg2-binary
@@ -79,7 +79,7 @@ commands =
7979
[testenv:database-relation-integration]
8080
description = Run database relation integration tests
8181
deps =
82-
juju
82+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
8383
pytest
8484
pytest-operator
8585
psycopg2-binary
@@ -90,7 +90,7 @@ commands =
9090
[testenv:db-relation-integration]
9191
description = Run db relation integration tests
9292
deps =
93-
juju
93+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
9494
pytest
9595
pytest-operator
9696
psycopg2-binary
@@ -101,7 +101,7 @@ commands =
101101
[testenv:db-admin-relation-integration]
102102
description = Run db-admin relation integration tests
103103
deps =
104-
juju
104+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
105105
pytest
106106
pytest-operator
107107
psycopg2-binary
@@ -112,7 +112,7 @@ commands =
112112
[testenv:password-rotation-integration]
113113
description = Run password rotation integration tests
114114
deps =
115-
juju
115+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
116116
pytest
117117
pytest-operator
118118
psycopg2-binary
@@ -123,7 +123,7 @@ commands =
123123
[testenv:tls-integration]
124124
description = Run TLS integration tests
125125
deps =
126-
juju
126+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
127127
pytest
128128
pytest-operator
129129
psycopg2-binary
@@ -134,7 +134,7 @@ commands =
134134
[testenv:integration]
135135
description = Run all integration tests
136136
deps =
137-
juju
137+
juju==2.9.11 # juju 3.0 has issues with retrieving action results and deploying charms
138138
pytest
139139
pytest-operator
140140
psycopg2-binary

0 commit comments

Comments
 (0)