Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1db665f
feat: starttls
JacobCoffee Mar 18, 2025
e655530
(broken) auto lets encrypt via acme state
JacobCoffee Apr 1, 2025
65dd645
(broken) correct commited result
JacobCoffee Apr 1, 2025
0dbf3ca
correct commited result again
JacobCoffee Apr 2, 2025
0aefff2
properly install PSF_CA certificate so certbot can use it
ewdurbin Apr 14, 2025
10a5006
uncomment working parts
JacobCoffee Apr 15, 2025
98dc839
stuff acme certs into appropriate pillar data
JacobCoffee Apr 21, 2025
ca751e4
feat: move acme stuff into pillar data
JacobCoffee Apr 23, 2025
50e70e7
fix: less suck, more good
JacobCoffee Apr 23, 2025
4d172e7
docs: explain a little
JacobCoffee Apr 24, 2025
536578a
fix: just one bugs section
JacobCoffee Apr 25, 2025
43ab883
fix: requires proper name to not break
JacobCoffee Apr 30, 2025
fa30753
feat: install certs alongside other certs.
JacobCoffee Apr 30, 2025
4d250dd
feat: read actual cert instead of config..
JacobCoffee Apr 30, 2025
bcb7bf6
chore: lint
JacobCoffee May 6, 2025
f2ab9c1
chore: lint again
JacobCoffee May 6, 2025
b740196
Merge branch 'main' into add-starttls
ewdurbin Oct 7, 2025
8d33d6c
fix: make acme_certs different from acme_certs 😵‍💫
JacobCoffee Oct 8, 2025
0da94f1
fix: use new, correct cert
JacobCoffee Oct 8, 2025
e2c1b7c
chore: cleanup star certs
JacobCoffee Oct 8, 2025
50af18e
feat: add services from lb
JacobCoffee Oct 8, 2025
ced030b
fix: dupe on planet
JacobCoffee Oct 8, 2025
33bb23a
cleanup things in fastly
JacobCoffee Oct 10, 2025
5eaa575
consolidate codespeed certs
ewdurbin Oct 10, 2025
2ec675e
literally just a nit
ewdurbin Oct 10, 2025
1ef51b5
consolidate legacy domains
ewdurbin Oct 10, 2025
daaee08
line breaks are free
ewdurbin Oct 10, 2025
a920973
alphabetize
ewdurbin Oct 10, 2025
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
1 change: 1 addition & 0 deletions pillar/dev/top.sls
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ base:
- tls
- users.*
- postgres.clusters
- pebble # needing to do this to have pebble rum in dev

'backup-server':
- match: nodegroup
Expand Down
228 changes: 193 additions & 35 deletions salt/_extensions/pillar/ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import binascii
import datetime
import os.path
from pathlib import Path

import salt.loader

Expand Down Expand Up @@ -295,46 +296,203 @@ def get_ca_signed_cert(cacert_path, ca_name, CN):
return "\n".join([cert, key])


def ext_pillar(minion_id, pillar, base="/etc/ssl", name="PSFCA", cert_opts=None):
if cert_opts is None:
cert_opts = {}
def _find_acme_certs(base_path="/etc/letsencrypt/live"):
"""Read ACME certificates from /etc/letsencrypt/live

returns dict with domain name (key) and data (value for each cert.
"""
acme_certs = {}
try:
if not Path(base_path).exists():
print(f"ACME base path {base_path} does not exist")
return acme_certs

print(f"Scanning for certificates in {base_path}")
for domain_dir in Path(base_path).iterdir():
try:
domain_dir_path = Path(base_path) / domain_dir
if not domain_dir_path.is_dir() or domain_dir.name == "README":
continue

domain_name = domain_dir.name
print(f"Found certificate directory: {domain_name}")

# use fullchain.pem instead of just cert.pem to include the full certificate chain
cert_file = domain_dir_path / "fullchain.pem"
key_file = domain_dir_path / "privkey.pem"

if not cert_file.exists():
print(f"Certificate file not found: {cert_file}")
continue

if not key_file.exists():
print(f"Key file not found: {key_file}")
continue

with cert_file.open('r') as f_cert:
cert_data = f_cert.read()

with key_file.open('r') as f_key:
key_data = f_key.read()

# Store combined certificate and key
combined_data = "\n".join([cert_data, key_data])
acme_certs[domain_name] = combined_data
# print(f"read certificate for {domain_name}")

except Exception as e:
print(f"Error processing certificate for {domain_dir.name}: {e}")

except Exception as e:
print(f"Error scanning ACME certificates directory: {e}")

print(f"Found {len(acme_certs)} ACME certificates")
return acme_certs


def _process_ca_certificates(minion_id, pillar, base="/etc/ssl", name="PSFCA", cert_opts=None):
ca_data = {
"ca": {},
"certs": {},
}

try:
if cert_opts is None:
cert_opts = {}

# Create CA certificate
opts = cert_opts.copy()
opts["CN"] = name
create_ca(base, name, **opts)

ca_data["ca"][name] = get_ca_cert(base, name)

# Process CA-signed certificates (gen_certs)
gen_certs = pillar.get("tls", {}).get("gen_certs", {})
for certificate, config in gen_certs.items():
role_patterns = [
role.get("pattern")
for role in [
pillar.get("roles", {}).get(r) for r in config.get("roles", "")
]
if role and role.get("pattern") is not None
]

if any(compound(pat, minion_id) for pat in role_patterns):
# Create the options
opts = cert_opts.copy()
opts["CN"] = certificate
opts["days"] = config.get("days", 1)

create_ca_signed_cert(base, name, **opts)

# Add the signed certificates to the pillar data
cert_data = get_ca_signed_cert(base, name, certificate)
ca_data["certs"][certificate] = cert_data
except Exception as e:
print(f"Error processing CA certificates: {e}")

return ca_data

# Ensure we have a CA created.
opts = cert_opts.copy()
opts["CN"] = name
create_ca(base, name, **opts)

# Start our pillar with just the ca certificate.
def _process_acme_certificates(minion_id, pillar):
"""Process ACME certificates

Reads ACME certificates and determines which ones should be available
to the specified minion based on access rules.
"""
acme_certs = {}

try:
print(f"Processing ACME certificates for minion: {minion_id}")
all_acme_certs = _find_acme_certs()

# Check if this is a loadbalancer (gets all certs)
Comment thread
ewdurbin marked this conversation as resolved.
Outdated
# todo: clean up all but the one that works
is_loadbalancer = False
try:
if 'loadbalancer' in minion_id.lower():
is_loadbalancer = True
print(f"Minion {minion_id} identified as loadbalancer by name")

# Also check via roles grain if that doesn't work
elif compound('G@roles:loadbalancer', minion_id):
is_loadbalancer = True
print(f"Minion {minion_id} identified as loadbalancer by grain")

# Additional check - look for the loadbalancer role in the hostname
elif (minion_id.startswith('lb.') or minion_id.startswith('loadbalancer.')):
is_loadbalancer = True
print(f"Minion {minion_id} identified as loadbalancer by hostname pattern")

if is_loadbalancer:
print(f"Minion {minion_id} is a loadbalancer, providing all certificates")
except Exception as e:
print(f"Error checking loadbalancer role: {e}")

# Process each certificate
for domain_name, cert_data in all_acme_certs.items():
should_include = False

# Loadbalancer gets all certs
if is_loadbalancer:
should_include = True
reason = "loadbalancer role"

# Minion name matches domain name
if minion_id.startswith(domain_name.split('.')[0]):
should_include = True
reason = "name match"

# Add certificate if allowed
if should_include:
acme_certs[domain_name] = cert_data
print(f"Added ACME certificate {domain_name} to pillar data (reason: {reason})")
else:
print(f"Skipping certificate {domain_name} for minion {minion_id} (no access)")

except Exception as e:
print(f"Error processing ACME certificates: {e}")

return acme_certs


def ext_pillar(minion_id, pillar, base="/etc/ssl", name="PSFCA", cert_opts=None):
"""Pillar extension to provide TLS certificates from internal PSFCA and acme.cert generated certs"""
print(f"Processing pillar data for minion: {minion_id}")

# initial data structure for certs
data = {
"tls": {
"ca": {
name: get_ca_cert(base, name),
},
"ca": {},
"certs": {},
"certs_acme": {},
},
}

# Create all of the certificates required by this minion
gen_certs = pillar.get("tls", {}).get("gen_certs", {})
for certificate, config in gen_certs.items():
role_patterns = [
role.get("pattern")
for role in [
pillar.get("roles", {}).get(r) for r in config.get("roles", "")
]
if role and role.get("pattern") is not None
]
if any([compound(pat, minion_id) for pat in role_patterns]):
# Create the options
opts = cert_opts.copy()
opts["CN"] = certificate
opts["days"] = config.get("days", 1)

# Create the signed certificates
create_ca_signed_cert(base, name, **opts)

# Add the signed certificates to the pillar data
cert_data = get_ca_signed_cert(base, name, certificate)
data["tls"]["certs"][certificate] = cert_data


# Process CA certificates and CA-signed certificates
ca_data = _process_ca_certificates(minion_id, pillar, base, name, cert_opts)
data["tls"]["ca"] = ca_data["ca"]
for cert_name, cert_data in ca_data["certs"].items():
data["tls"]["certs"][cert_name] = cert_data

# process ACME certificates
acme_certs = _process_acme_certificates(minion_id, pillar)

# Add ACME certificates to both certs and certs_acme sections
for cert_name, cert_data in acme_certs.items():
# Store in certs_acme section (dedicated for ACME certificates)
data["tls"]["certs_acme"][cert_name] = cert_data

# Also store in general certs section for backward compatibility
# Only if not already present from CA-signed certs
if cert_name not in data["tls"]["certs"]:
data["tls"]["certs"][cert_name] = cert_data

# Check if we have ACME certificates for debugging
if not acme_certs:
print(f"No ACME certificates were included for minion: {minion_id}")
else:
print(f"Included {len(acme_certs)} ACME certificates for minion: {minion_id}")

return data
4 changes: 2 additions & 2 deletions salt/bugs/config/postfix/main.cf
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ compatibility_level = 3.6


# TLS parameters
smtpd_tls_cert_file=ssl_certificate /etc/ssl/private/bugs.psf.io.pem;
smtpd_tls_key_file=etc/ssl/private/bugs.psf.io.pem;
smtpd_tls_cert_file=/etc/ssl/private/bugs.psf.io.pem
Comment thread
ewdurbin marked this conversation as resolved.
Outdated
Comment thread
JacobCoffee marked this conversation as resolved.
Outdated
smtpd_tls_key_file=/etc/ssl/private/bugs.psf.io.pem
smtpd_tls_security_level=may

smtp_tls_CApath=/etc/ssl/certs
Expand Down
107 changes: 107 additions & 0 deletions salt/tls/init.sls
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
include:
- .pebble
- .lego

ssl-cert:
pkg.installed

certbot:
Comment thread
JacobCoffee marked this conversation as resolved.
pkg.installed

{% for name in salt["pillar.get"]("tls:ca", {}) %} # " Syntax Hack
/etc/ssl/certs/{{ name }}.pem:
Expand All @@ -11,8 +17,21 @@ ssl-cert:
- mode: "0644"
- require:
- pkg: ssl-cert

/usr/local/share/ca-certificates/{{ name }}.crt:
file.managed:
- contents_pillar: tls:ca:{{ name }}
- user: root
- group: ssl-cert
- mode: "0644"
- require:
- pkg: ssl-cert
{% endfor %}

/usr/sbin/update-ca-certificates:
cmd.wait:
- watch:
- file: /usr/local/share/ca-certificates/*.crt

{% for name in salt["pillar.get"]("tls:certs", {}) %} # " Syntax Hack
/etc/ssl/private/{{ name }}.pem:
Expand All @@ -25,3 +44,91 @@ ssl-cert:
- require:
- pkg: ssl-cert
{% endfor %}

{% if salt["match.compound"](pillar["roles"]["salt-master"]["pattern"]) %}
# HTTP-validated domains
{% for domain in [
'pypa.io',
Comment thread
ewdurbin marked this conversation as resolved.
Outdated
'www.pycon.org',
'speed.pypy.org',
'salt-public.psf.io',
'planetpython.org',
'bugs.python.org'
] %}
{{ domain }}:
acme.cert:
- email: infrastructure-staff@python.org
- webroot: /etc/lego
- renew: 14
{% if pillar["dc"] == "vagrant" %}
- server: https://salt-master.vagrant.psf.io:14000/dir
Comment thread
ewdurbin marked this conversation as resolved.
{% endif %}
- require:
- sls: tls.lego
{% endfor %}

# DNS-validated domains
# dns plugins do not exist yet for route53 & gandi
{#star.python.org:#}
{# acme.cert:#}
{# - aliases:#}
{# - python.org#}
{# - email: infrastructure-staff@python.org#}
{## - dns_plugin: route53#}
{## - dns_plugin_credentials: route53.python#}
{# - renew: 14#}
{# - server: https://localhost:14000/dir#}
{# - require:#}
{# - pkg: certbot#}
{#
- sls: tls.lego
{#star.pycon.org:#}
{# acme.cert:#}
{# - aliases:#}
{# - pycon.org#}
{# - email: infrastructure-staff@python.org#}
{## - dns_plugin: route53#}
{## - dns_plugin_credentials: route53.pycon#}
{# - renew: 14#}
{# - server: https://localhost:14000/dir#}
{# - require:#}
{# - sls: tls.lego#}

{#star.pyfound.org:#}
{# acme.cert:#}
{# - aliases:#}
{# - pyfound.org#}
{# - email: infrastructure-staff@python.org#}
{## - dns_plugin: gandiv5#}
{## - dns_plugin_credentials: gandi#}
{# - renew: 14#}
{# - require:#}
{# - sls: tls.lego#}

# Multi-domain certificates
{#jython.org:#}
{# acme.cert:#}
{# - aliases:#}
{# - www.jython.net#}
{# - jython.net#}
{# - www.jython.com#}
{# - jython.com#}
{# - email: infrastructure-staff@python.org#}
{# - webroot: /etc/lego#}
{# - renew: 14#}
{# - require:#}
{# - sls: tls.lego#}
{##}
{#bugs.python.org-multi:#}
{# acme.cert:#}
{# - name: bugs.python.org#}
{# - aliases:#}
{# - bugs.jython.org#}
{# - issues.roundup-tracker.org#}
{# - mail.roundup-tracker.org#}
{# - email: infrastructure-staff@python.org#}
{# - webroot: /etc/lego#}
{# - renew: 14#}
{# - require:#}
{# - sls: tls.lego#}
{% endif %}
Loading
Loading