From 937ce744b055bfd0575266a3f850aa15bae3f190 Mon Sep 17 00:00:00 2001 From: Manoel Benedito Neto Date: Wed, 28 Feb 2024 16:14:17 -0300 Subject: [PATCH] Implement IPsec Cert-Renewal Operation This commit adds IPsec Cert-Renewal implementation to work properly when specified by "--opcode" parameter in IPsec client execution. This implementation adds to IPsec client a rekey step after the generated keys and cert are stored and exchanged during cert-renewal operation. The main goal of this implementation is to provide new certificates and keys for an IPsec client host that has already been authenticated by IPsec server host. Test Plan: PASS: Full build, system install, bootstrap and unlock DX system w/ unlocked enabled available status. PASS: Execute "ipsec-client pxecontroller --opcode 2" in controller-1. Observe the previously created CertificateRequest was deleted and generated a new one for controller-1's node. The new certificate is sent to IPsec Client and stored with the swanctl rekey command executed sucessfully. PASS: In a DC system with available enabled active status with IPsec server being executed from controller-0. Change c0 and c1 dates to expire IPsec certificates. If needed, recover kubernetes certificates or pods. Execute "sudo ipsec-client pxecontroller -o 2" command from controller-0 and controller-1. Observe that certificates and keys were generated and stored in /etc/swanctl/ directory. Observe new SAs have been created between controllers by executing "sudo swanctl --list-sas" command. Story: 2010940 Task: 49656 Change-Id: I69383005c2e204fe0a6401b2efaf05e8754f2bc3 Signed-off-by: Manoel Benedito Neto --- .../sysinv/sysinv/sysinv/cmd/ipsec_client.py | 2 +- .../sysinv/sysinv/ipsec_auth/client/client.py | 66 +++++++++++++------ .../sysinv/sysinv/ipsec_auth/client/config.py | 4 +- .../sysinv/ipsec_auth/common/constants.py | 4 +- .../sysinv/sysinv/ipsec_auth/common/utils.py | 10 +-- .../sysinv/sysinv/ipsec_auth/server/server.py | 12 ++-- 6 files changed, 64 insertions(+), 34 deletions(-) diff --git a/sysinv/sysinv/sysinv/sysinv/cmd/ipsec_client.py b/sysinv/sysinv/sysinv/sysinv/cmd/ipsec_client.py index e5dd6746a7..9213e5af99 100644 --- a/sysinv/sysinv/sysinv/sysinv/cmd/ipsec_client.py +++ b/sysinv/sysinv/sysinv/sysinv/cmd/ipsec_client.py @@ -46,7 +46,7 @@ def main(): help="If enabled, the logging level will be set " "to DEBUG instead of the default INFO level.") parser.add_argument("-o", "--opcode", metavar='', - type=int, choices=[1, 2, 3], + type=int, choices=[1, 2], help='Operational code (Default: ' + str(opcode) + ')') args = parser.parse_args() diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py index 73487b73ee..5399160bf0 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py @@ -29,10 +29,10 @@ LOG = logging.getLogger(__name__) class Client(object): - def __init__(self, host, port, opcode): + def __init__(self, host, port, op_code): self.host = host self.port = port - self.opcode = opcode + self.op_code = str(op_code) self.state = State.STAGE_1 self.ifname = utils.get_management_interface() self.personality = utils.get_personality() @@ -45,7 +45,7 @@ class Client(object): # Generate message 1 - OP/MAC/HASH def _generate_message_1(self): message = {} - message['op'] = str(self.opcode) + message['op'] = self.op_code message['mac_addr'] = self.mac_addr message['hash'] = utils.hash_payload(message) @@ -159,12 +159,15 @@ class Client(object): if self.state == State.STAGE_4: LOG.info("Received IPSec Auth CSR Response") cert = base64.b64decode(msg['cert']) - network = msg['network'] digest = base64.b64decode(msg['hash']) ca_cert = utils.load_data(constants.TRUSTED_CA_CERT_PATH) - data = msg['cert'].encode('utf-8') + network.encode('utf-8') + data = msg['cert'].encode('utf-8') + if self.op_code == constants.OP_CODE_INITIAL_AUTH: + network = msg['network'] + data = data + network.encode('utf-8') + if not utils.verify_signed_hash(ca_cert, digest, data): msg = "Hash validation failed" LOG.exception("Hash validation failed") @@ -176,23 +179,46 @@ class Client(object): cert_path = constants.CERT_SYSTEM_LOCAL_DIR + cert_file utils.save_data(cert_path, cert) - if self.personality == constants.CONTROLLER: - self.local_addr = self.hostname[constants.UNIT_HOSTNAME] + ', ' \ - + self.hostname[constants.FLOATING_UNIT_HOSTNAME] - else: - self.local_addr = utils.get_ip_addr(self.ifname) + if self.op_code == constants.OP_CODE_INITIAL_AUTH: + if self.personality == constants.CONTROLLER: + self.local_addr = self.hostname[constants.UNIT_HOSTNAME] + ', ' \ + + self.hostname[constants.FLOATING_UNIT_HOSTNAME] + else: + self.local_addr = utils.get_ip_addr(self.ifname) - LOG.info("Generating config files and restart ipsec") - strong = config.StrongswanPuppet(self.hostname[constants.UNIT_HOSTNAME], - self.local_addr, network) - strong.generate_file() - puppet_cf = subprocess.run(['puppet', 'apply', '-e', 'include ::platform::strongswan'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + LOG.info("Generating config files and restart ipsec") + strong = config.StrongswanPuppet(self.hostname[constants.UNIT_HOSTNAME], + self.local_addr, network) + strong.generate_file() + puppet_cf = subprocess.run(['puppet', 'apply', '-e', + 'include ::platform::strongswan'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) - if puppet_cf.stderr: - err = "Error: %s" % (puppet_cf.stderr.decode("utf-8")) - LOG.exception("Failed to create strongswan config files: %s" % err) - return False + if puppet_cf.returncode != 0: + err = "Error: %s" % (puppet_cf.stderr.decode("utf-8")) + LOG.exception("Failed to create StrongSwan config files: %s" % err) + return False + + elif self.op_code == constants.OP_CODE_CERT_RENEWAL: + load_creds = subprocess.run(['swanctl', '--load-creds'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False) + + if load_creds.returncode != 0: + err = "Error: %s" % (load_creds.stderr.decode("utf-8")) + LOG.exception("Failed to load StrongSwan credentials: %s" % err) + return False + + rekey = subprocess.run(['swanctl', '--rekey', '--ike', constants.IKE_SA_NAME, + '--reauth'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False) + + if rekey.returncode != 0: + err = "Error: %s" % (rekey.stderr.decode("utf-8")) + LOG.exception("Failed to rekey IKE SA with StrongSwan: %s" % err) + return False + + LOG.info('IPsec certificate renewed successfully') return True diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/config.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/config.py index 9fd908cce9..0242ff962b 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/config.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/config.py @@ -90,11 +90,11 @@ class SwanctlConf(object): self.node[key] = value def get_conf(self): - self.children['node'] = self.node + self.children[constants.CHILD_SA_NAME] = self.node self.system_nodes['local'] = self.local self.system_nodes['remote'] = self.remote self.system_nodes['children'] = self.children - self.connections['system-nodes'] = self.system_nodes + self.connections[constants.IKE_SA_NAME] = self.system_nodes return self.connections diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py index 5141fe73e4..4bde84f949 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py @@ -49,10 +49,12 @@ PXECONTROLLER_URL = 'http://pxecontroller:6385' OP_CODE_INITIAL_AUTH = "1" OP_CODE_CERT_RENEWAL = "2" - SUPPORTED_OP_CODES = [OP_CODE_INITIAL_AUTH, OP_CODE_CERT_RENEWAL] MGMT_IPSEC_ENABLING = 'enabling' MGMT_IPSEC_ENABLED = 'enabled' MGMT_IPSEC_DISABLED = 'disabled' + +CHILD_SA_NAME = 'node' +IKE_SA_NAME = 'system-nodes' diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py index 1af2db6d66..ec626c185a 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py @@ -282,7 +282,7 @@ def kube_apply_certificate_request(body): elif get_cr.stderr and 'NotFound' not in str(get_cr.stderr): err = "Error: %s" % (get_cr.stderr.decode("utf-8")) LOG.exception("Failed to retrieve CertificateRequest resource info. %s" % (err)) - return + return None # Create CertificateRequest resource in kubernetes cr_body = yaml.safe_dump(body, default_flow_style=False) @@ -292,11 +292,11 @@ def kube_apply_certificate_request(body): stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) - if create_cr.stderr: + if create_cr.returncode != 0: err = "Error: %s" % (create_cr.stderr.decode("utf-8")) LOG.exception("Failed to create CertificateRequest %s/%s. %s" % (constants.NAMESPACE_DEPLOYMENT, name, err)) - return + return None # Get Certificate from recently created resource in kubernetes cmd_get_certificate = ['-o', "jsonpath='{.status.certificate}'"] @@ -305,10 +305,10 @@ def kube_apply_certificate_request(body): stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) - if signed_cert.stderr: + if signed_cert.returncode != 0: err = "Error: %s" % (signed_cert.stderr.decode("utf-8")) LOG.exception("Failed to retrieve %s/%s's Certificate. %s" % (constants.NAMESPACE_DEPLOYMENT, name, err)) - return + return None return signed_cert.stdout.decode("utf-8").strip("'") diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py index 39680d8a01..3d50a183c1 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py @@ -90,6 +90,7 @@ class IPsecConnection(object): CA_CRT = 'tls.crt' def __init__(self): + self.op_code = None self.hostname = None self.mgmt_subnet = None self.signed_cert = None @@ -153,7 +154,6 @@ class IPsecConnection(object): raise ConnectionRefusedError(msg) client_data = utils.get_client_hostname_and_mgmt_subnet(mac_addr) - self.hostname = client_data['hostname'] self.mgmt_subnet = client_data['mgmt_subnet'] @@ -199,12 +199,14 @@ class IPsecConnection(object): if not self.signed_cert: raise ValueError('Unable to sign certificate request') - cert = bytes(self.signed_cert, 'utf-8') - network = bytes(self.mgmt_subnet, 'utf-8') - hash_payload = utils.hash_and_sign_payload(self.ca_key, cert + network) + data = bytes(self.signed_cert, 'utf-8') + if self.op_code == constants.OP_CODE_INITIAL_AUTH: + payload["network"] = self.mgmt_subnet + data = data + bytes(self.mgmt_subnet, 'utf-8') + + hash_payload = utils.hash_and_sign_payload(self.ca_key, data) payload["cert"] = self.signed_cert - payload["network"] = self.mgmt_subnet payload["hash"] = hash_payload.decode("utf-8") LOG.info("Sending IPSec Auth CSR Response")