Skip to content

Commit 50e70e7

Browse files
committed
fix: less suck, more good
1 parent ca751e4 commit 50e70e7

File tree

1 file changed

+44
-193
lines changed
  • salt/_extensions/pillar

1 file changed

+44
-193
lines changed

salt/_extensions/pillar/ca.py

Lines changed: 44 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import binascii
44
import datetime
55
import os.path
6-
from pathlib import Path
76

87
import salt.loader
98

@@ -296,203 +295,55 @@ def get_ca_signed_cert(cacert_path, ca_name, CN):
296295
return "\n".join([cert, key])
297296

298297

299-
def _find_acme_certs(base_path="/etc/letsencrypt/live"):
300-
"""Read ACME certificates from /etc/letsencrypt/live
301-
302-
returns dict with domain name (key) and data (value for each cert.
303-
"""
304-
acme_certs = {}
305-
try:
306-
if not Path(base_path).exists():
307-
print(f"ACME base path {base_path} does not exist")
308-
return acme_certs
309-
310-
print(f"Scanning for certificates in {base_path}")
311-
for domain_dir in Path(base_path).iterdir():
312-
try:
313-
domain_dir_path = Path(base_path) / domain_dir
314-
if not domain_dir_path.is_dir() or domain_dir.name == "README":
315-
continue
316-
317-
domain_name = domain_dir.name
318-
print(f"Found certificate directory: {domain_name}")
319-
320-
# use fullchain.pem instead of just cert.pem to include the full certificate chain
321-
cert_file = domain_dir_path / "fullchain.pem"
322-
key_file = domain_dir_path / "privkey.pem"
323-
324-
if not cert_file.exists():
325-
print(f"Certificate file not found: {cert_file}")
326-
continue
327-
328-
if not key_file.exists():
329-
print(f"Key file not found: {key_file}")
330-
continue
331-
332-
with cert_file.open('r') as f_cert:
333-
cert_data = f_cert.read()
334-
335-
with key_file.open('r') as f_key:
336-
key_data = f_key.read()
337-
338-
# Store combined certificate and key
339-
combined_data = "\n".join([cert_data, key_data])
340-
acme_certs[domain_name] = combined_data
341-
# print(f"read certificate for {domain_name}")
342-
343-
except Exception as e:
344-
print(f"Error processing certificate for {domain_dir.name}: {e}")
345-
346-
except Exception as e:
347-
print(f"Error scanning ACME certificates directory: {e}")
348-
349-
print(f"Found {len(acme_certs)} ACME certificates")
350-
return acme_certs
351-
352-
353-
def _process_ca_certificates(minion_id, pillar, base="/etc/ssl", name="PSFCA", cert_opts=None):
354-
ca_data = {
355-
"ca": {},
356-
"certs": {},
357-
}
358-
359-
try:
360-
if cert_opts is None:
361-
cert_opts = {}
362-
363-
# Create CA certificate
364-
opts = cert_opts.copy()
365-
opts["CN"] = name
366-
create_ca(base, name, **opts)
367-
368-
ca_data["ca"][name] = get_ca_cert(base, name)
369-
370-
# Process CA-signed certificates (gen_certs)
371-
gen_certs = pillar.get("tls", {}).get("gen_certs", {})
372-
for certificate, config in gen_certs.items():
373-
role_patterns = [
374-
role.get("pattern")
375-
for role in [
376-
pillar.get("roles", {}).get(r) for r in config.get("roles", "")
377-
]
378-
if role and role.get("pattern") is not None
379-
]
380-
381-
if any(compound(pat, minion_id) for pat in role_patterns):
382-
# Create the options
383-
opts = cert_opts.copy()
384-
opts["CN"] = certificate
385-
opts["days"] = config.get("days", 1)
386-
387-
create_ca_signed_cert(base, name, **opts)
388-
389-
# Add the signed certificates to the pillar data
390-
cert_data = get_ca_signed_cert(base, name, certificate)
391-
ca_data["certs"][certificate] = cert_data
392-
except Exception as e:
393-
print(f"Error processing CA certificates: {e}")
394-
395-
return ca_data
396-
397-
398-
def _process_acme_certificates(minion_id, pillar):
399-
"""Process ACME certificates
400-
401-
Reads ACME certificates and determines which ones should be available
402-
to the specified minion based on access rules.
403-
"""
404-
acme_certs = {}
405-
406-
try:
407-
print(f"Processing ACME certificates for minion: {minion_id}")
408-
all_acme_certs = _find_acme_certs()
409-
410-
# Check if this is a loadbalancer (gets all certs)
411-
# todo: clean up all but the one that works
412-
is_loadbalancer = False
413-
try:
414-
if 'loadbalancer' in minion_id.lower():
415-
is_loadbalancer = True
416-
print(f"Minion {minion_id} identified as loadbalancer by name")
417-
418-
# Also check via roles grain if that doesn't work
419-
elif compound('G@roles:loadbalancer', minion_id):
420-
is_loadbalancer = True
421-
print(f"Minion {minion_id} identified as loadbalancer by grain")
422-
423-
# Additional check - look for the loadbalancer role in the hostname
424-
elif (minion_id.startswith('lb.') or minion_id.startswith('loadbalancer.')):
425-
is_loadbalancer = True
426-
print(f"Minion {minion_id} identified as loadbalancer by hostname pattern")
427-
428-
if is_loadbalancer:
429-
print(f"Minion {minion_id} is a loadbalancer, providing all certificates")
430-
except Exception as e:
431-
print(f"Error checking loadbalancer role: {e}")
432-
433-
# Process each certificate
434-
for domain_name, cert_data in all_acme_certs.items():
435-
should_include = False
436-
437-
# Loadbalancer gets all certs
438-
if is_loadbalancer:
439-
should_include = True
440-
reason = "loadbalancer role"
441-
442-
# Minion name matches domain name
443-
if minion_id.startswith(domain_name.split('.')[0]):
444-
should_include = True
445-
reason = "name match"
446-
447-
# Add certificate if allowed
448-
if should_include:
449-
acme_certs[domain_name] = cert_data
450-
print(f"Added ACME certificate {domain_name} to pillar data (reason: {reason})")
451-
else:
452-
print(f"Skipping certificate {domain_name} for minion {minion_id} (no access)")
453-
454-
except Exception as e:
455-
print(f"Error processing ACME certificates: {e}")
456-
457-
return acme_certs
298+
def ext_pillar(minion_id, pillar, base="/etc/ssl", name="PSFCA", cert_opts=None):
299+
if cert_opts is None:
300+
cert_opts = {}
458301

302+
# Create CA certificate
303+
opts = cert_opts.copy()
304+
opts["CN"] = name
305+
create_ca(base, name, **opts)
459306

460-
def ext_pillar(minion_id, pillar, base="/etc/ssl", name="PSFCA", cert_opts=None):
461-
"""Pillar extension to provide TLS certificates from internal PSFCA and acme.cert generated certs"""
462-
print(f"Processing pillar data for minion: {minion_id}")
463-
464-
# initial data structure for certs
465307
data = {
466308
"tls": {
467-
"ca": {},
309+
"ca": {
310+
name: get_ca_cert(base, name),
311+
},
468312
"certs": {},
469-
"certs_acme": {},
313+
"acme_certs": {},
470314
},
471315
}
472-
473-
# Process CA certificates and CA-signed certificates
474-
ca_data = _process_ca_certificates(minion_id, pillar, base, name, cert_opts)
475-
data["tls"]["ca"] = ca_data["ca"]
476-
for cert_name, cert_data in ca_data["certs"].items():
477-
data["tls"]["certs"][cert_name] = cert_data
478-
479-
# process ACME certificates
480-
acme_certs = _process_acme_certificates(minion_id, pillar)
481-
482-
# Add ACME certificates to both certs and certs_acme sections
483-
for cert_name, cert_data in acme_certs.items():
484-
# Store in certs_acme section (dedicated for ACME certificates)
485-
data["tls"]["certs_acme"][cert_name] = cert_data
486-
487-
# Also store in general certs section for backward compatibility
488-
# Only if not already present from CA-signed certs
489-
if cert_name not in data["tls"]["certs"]:
490-
data["tls"]["certs"][cert_name] = cert_data
491-
492-
# Check if we have ACME certificates for debugging
493-
if not acme_certs:
494-
print(f"No ACME certificates were included for minion: {minion_id}")
495-
else:
496-
print(f"Included {len(acme_certs)} ACME certificates for minion: {minion_id}")
497-
316+
317+
minion_roles = []
318+
minion_roles.extend(
319+
role_name
320+
for role_name, role_config in pillar.get("roles", {}).items()
321+
if role_config.get("pattern")
322+
and compound(role_config["pattern"], minion_id)
323+
)
324+
325+
# Process CA-signed certificates (gen_certs)
326+
gen_certs = pillar.get("tls", {}).get("gen_certs", {})
327+
for certificate, config in gen_certs.items():
328+
cert_roles = config.get("roles", [])
329+
# Check if any of the minion's roles are in the certificate's required roles
330+
if any(role in minion_roles for role in cert_roles):
331+
# Create the options
332+
opts = cert_opts.copy()
333+
opts["CN"] = certificate
334+
opts["days"] = config.get("days", 1)
335+
336+
create_ca_signed_cert(base, name, **opts)
337+
338+
# Add the signed certificates to the pillar data
339+
cert_data = get_ca_signed_cert(base, name, certificate)
340+
data["tls"]["certs"][certificate] = cert_data
341+
342+
# Collect ACME certs (acme.cert) for this minion based on its roles
343+
acme_certs = pillar.get("tls", {}).get("acme_certs", {})
344+
for domain, domain_config in acme_certs.items():
345+
cert_roles = domain_config.get("roles", [])
346+
if any(role in minion_roles for role in cert_roles):
347+
data["tls"]["acme_certs"][domain] = domain_config
348+
498349
return data

0 commit comments

Comments
 (0)