Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TLS passthrough #67

Merged
merged 24 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
56ce685
Add prompt for ssl certificate and key for HAProxy, add to tf variabl…
wyattrees Jun 21, 2024
478f512
Use separate terraform variable to set services yaml
wyattrees Jul 10, 2024
3e921df
Formatting
wyattrees Jul 10, 2024
8fcfb81
suggestions
skatsaounis Jul 15, 2024
6b924f3
Add https redirect, bind agent service to IP address in local network…
wyattrees Aug 1, 2024
6318e35
get management cdirs from database. better error handling around veri…
wyattrees Aug 14, 2024
b89840d
Add configuration option for maas-region to setup TLS Termination on …
wyattrees Aug 26, 2024
480cc4d
Add question for tls mode
wyattrees Aug 27, 2024
ad221dd
Pass ssl_cert and ssl_key from haproxy question bank to maas-region c…
wyattrees Aug 28, 2024
f46eb28
Pass contents of ssl key/cert to maas-region variables instead of fil…
wyattrees Sep 4, 2024
fb2f7dd
Merge cleanup
wyattrees Sep 4, 2024
d0f7b3c
remove duplicate tls_mode variable in maas-region variables.tf
wyattrees Sep 4, 2024
022f124
revert default values for ssl_cert and ssl_key question answers to em…
wyattrees Sep 5, 2024
9acb0ec
Save ssl_cert and ssl_key in variables, load content in extra_tfvars
wyattrees Sep 6, 2024
32c57db
Ask tls questions during maas-region init step if haproxy unit is not…
wyattrees Sep 9, 2024
c925753
Allow empty string for ssl cert/key, question prompt for tls mode dis…
wyattrees Sep 9, 2024
907c7d7
lint
wyattrees Sep 10, 2024
fd8e317
check file existence in try/catch block instead of using os.path.is_file
wyattrees Sep 26, 2024
cc11be7
add back in basic validation for CA cert chain. Include tls questions…
wyattrees Oct 3, 2024
0886324
Cleanup
wyattrees Oct 3, 2024
e626f2a
Revert maas-region version
wyattrees Oct 3, 2024
55982a5
fixing default value of tls_mode in maas-region variables.tf
wyattrees Nov 13, 2024
6e3bc4b
Merge branch 'main' into tls-passthrough
wyattrees Nov 13, 2024
7da708e
remove question mark from question
wyattrees Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 87 additions & 34 deletions anvil-python/anvil/commands/haproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@

import ipaddress
import logging
import os.path
from typing import Any, List
from typing import Any, Callable, List

from rich.console import Console
from sunbeam.clusterd.client import Client
from sunbeam.commands.juju import BOOTSTRAP_CONFIG_KEY
from sunbeam.commands.terraform import TerraformInitStep
from sunbeam.commands.terraform import TerraformException, TerraformInitStep
from sunbeam.jobs import questions
from sunbeam.jobs.common import BaseStep, ResultType
from sunbeam.jobs.juju import JujuHelper
Expand All @@ -42,31 +41,44 @@
HAPROXY_UNIT_TIMEOUT = (
1200 # 15 minutes, adding / removing units can take a long time
)
LOG = logging.getLogger(__name__)
HAPROXY_VALID_TLS_MODES = ["termination", "passthrough", "disabled"]


def validate_cert_file(filepath: str) -> None:
if filepath == "":
return
if not os.path.isfile(filepath):
raise ValueError(f"{filepath} does not exist")
try:
with open(filepath) as f:
if "BEGIN CERTIFICATE" not in f.read():
raise ValueError("Invalid certificate file")
except FileNotFoundError:
raise ValueError(f"{filepath} does not exist")
except PermissionError:
raise ValueError(f"Permission denied when trying to read {filepath}")


def validate_key_file(filepath: str) -> None:
if filepath == "":
return
if not os.path.isfile(filepath):
raise ValueError(f"{filepath} does not exist")
try:
with open(filepath) as f:
if "BEGIN PRIVATE KEY" not in f.read():
raise ValueError("Invalid key file")
except FileNotFoundError:
raise ValueError(f"{filepath} does not exist")
except PermissionError:
raise ValueError(f"Permission denied when trying to read {filepath}")


def validate_cacert_chain(filepath: str) -> None:
if filepath == "":
return
try:
with open(filepath) as f:
if "BEGIN CERTIFICATE" not in f.read():
raise ValueError("Invalid CA certificate file")
except FileNotFoundError:
raise ValueError(f"{filepath} does not exist")
except PermissionError:
raise ValueError(f"Permission denied when trying to read {filepath}")
wyattrees marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -81,23 +93,46 @@ def validate_virtual_ip(value: str) -> None:
raise ValueError(f"{value} is not a valid IP address: {e}")


def haproxy_questions() -> dict[str, questions.PromptQuestion]:
def get_validate_tls_mode_fn(valid_modes: list[str]) -> Callable[[str], None]:
def validate_tls_mode(value: str) -> None:
if value not in valid_modes:
raise ValueError(f"TLS Mode must be one of {valid_modes}")

return validate_tls_mode


def tls_questions(tls_modes: list[str]) -> dict[str, questions.PromptQuestion]:
return {
"virtual_ip": questions.PromptQuestion(
"Virtual IP to use for the Cluster in HA",
default_value="",
validation_function=validate_virtual_ip,
),
"ssl_cert": questions.PromptQuestion(
"Path to SSL Certificate for HAProxy (enter nothing to skip TLS)",
"Path to SSL Certificate for HAProxy",
default_value="",
validation_function=validate_cert_file,
),
"ssl_key": questions.PromptQuestion(
"Path to private key for the SSL certificate (enter nothing to skip TLS)",
"Path to private key for the SSL certificate",
default_value="",
validation_function=validate_key_file,
),
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
"ssl_cacert": questions.PromptQuestion(
"Path to CA cert chain, for use with self-signed SSL certificates (enter nothing to skip)",
default_value="",
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
validation_function=validate_cacert_chain,
),
"tls_mode": questions.PromptQuestion(
f"TLS mode: {tls_modes}?",
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
default_value="disabled",
validation_function=get_validate_tls_mode_fn(tls_modes),
),
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
}


def haproxy_questions() -> dict[str, questions.PromptQuestion]:
return {
"virtual_ip": questions.PromptQuestion(
"Virtual IP to use for the Cluster in HA",
default_value="",
validation_function=validate_virtual_ip,
)
}


Expand Down Expand Up @@ -154,24 +189,33 @@ def prompt(self, console: Console | None = None) -> None:
variables.setdefault("virtual_ip", "")
variables.setdefault("ssl_cert", "")
variables.setdefault("ssl_key", "")
variables.setdefault("ssl_cacert", "")
variables.setdefault("tls_mode", "disabled")

# Set defaults
self.preseed.setdefault("virtual_ip", "")
self.preseed.setdefault("ssl_cert", "")
self.preseed.setdefault("ssl_key", "")
self.preseed.setdefault("ssl_cacert", "")
self.preseed.setdefault("tls_mode", "disabled")

qs = haproxy_questions()
qs.update(tls_questions(HAPROXY_VALID_TLS_MODES))
haproxy_config_bank = questions.QuestionBank(
questions=haproxy_questions(),
questions=qs,
console=console,
preseed=self.preseed.get("haproxy"),
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
previous_answers=variables,
accept_defaults=self.accept_defaults,
)

cert_filepath = haproxy_config_bank.ssl_cert.ask()
variables["ssl_cert"] = cert_filepath
key_filepath = haproxy_config_bank.ssl_key.ask()
variables["ssl_key"] = key_filepath
tls_mode = haproxy_config_bank.tls_mode.ask()
variables["tls_mode"] = tls_mode
if tls_mode != "disabled":
variables["ssl_cert"] = haproxy_config_bank.ssl_cert.ask()
variables["ssl_key"] = haproxy_config_bank.ssl_key.ask()
if tls_mode == "passthrough":
variables["ssl_cacert"] = haproxy_config_bank.ssl_cacert.ask()
virtual_ip = haproxy_config_bank.virtual_ip.ask()
variables["virtual_ip"] = virtual_ip

Expand All @@ -183,21 +227,27 @@ def extra_tfvars(self) -> dict[str, Any]:
self.client, self._HAPROXY_CONFIG
)

cert_filepath = variables["ssl_cert"]
key_filepath = variables["ssl_key"]
if cert_filepath != "" and key_filepath != "":
with open(cert_filepath) as cert_file:
if variables["tls_mode"] != "disabled":
variables["haproxy_port"] = 443
variables["haproxy_services_yaml"] = self.get_tls_services_yaml(
variables["tls_mode"]
)
if not variables["ssl_cert"] or not variables["ssl_key"]:
raise TerraformException(
"Both ssl_cert and ssl_key must be provided when enabling TLS"
)
with open(variables["ssl_cert"]) as cert_file:
variables["ssl_cert_content"] = cert_file.read()
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
with open(key_filepath) as key_file:
with open(variables["ssl_key"]) as key_file:
variables["ssl_key_content"] = key_file.read()
variables["haproxy_port"] = 443
variables["haproxy_services_yaml"] = self.get_tls_services_yaml()
else:
variables["haproxy_port"] = 80

# Terraform does not need the content of these answers
variables.pop("ssl_cert", None)
wyattrees marked this conversation as resolved.
Show resolved Hide resolved
variables.pop("ssl_key", None)
variables.pop("tls_mode", "disabled")
variables.pop("ssl_cert", "")
variables.pop("ssl_key", "")
variables.pop("ssl_cacert", "")

LOG.debug(f"extra tfvars: {variables}")
return variables
Expand All @@ -209,7 +259,7 @@ def get_management_cidrs(self) -> list[str]:
)
return answers["bootstrap"]["management_cidr"].split(",")

def get_tls_services_yaml(self) -> str:
def get_tls_services_yaml(self, tls_mode: str) -> str:
"""Get the HAProxy services.yaml for TLS, inserting the VIP for the frontend bind"""
cidrs = self.get_management_cidrs()
services: str = (
Expand All @@ -219,9 +269,12 @@ def get_tls_services_yaml(self) -> str:
service_options:
- balance leastconn
- cookie SRVNAME insert
- http-request redirect scheme https unless { ssl_fc }
server_options: maxconn 100 cookie S{i} check
crts: [DEFAULT]
- http-request redirect scheme https unless { ssl_fc }"""
+ ("\n - mode tcp" if tls_mode == "passthrough" else "")
+ """
server_options: maxconn 100 cookie S{i} check"""
+ ("\n crts: [DEFAULT]" if tls_mode == "termination" else "")
+ """
- service_name: agent_service
service_host: 0.0.0.0
service_port: 80
Expand Down
Loading
Loading