|
3 | 3 | import binascii |
4 | 4 | import datetime |
5 | 5 | import os.path |
6 | | -from pathlib import Path |
7 | 6 |
|
8 | 7 | import salt.loader |
9 | 8 |
|
@@ -296,203 +295,55 @@ def get_ca_signed_cert(cacert_path, ca_name, CN): |
296 | 295 | return "\n".join([cert, key]) |
297 | 296 |
|
298 | 297 |
|
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 = {} |
458 | 301 |
|
| 302 | + # Create CA certificate |
| 303 | + opts = cert_opts.copy() |
| 304 | + opts["CN"] = name |
| 305 | + create_ca(base, name, **opts) |
459 | 306 |
|
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 |
465 | 307 | data = { |
466 | 308 | "tls": { |
467 | | - "ca": {}, |
| 309 | + "ca": { |
| 310 | + name: get_ca_cert(base, name), |
| 311 | + }, |
468 | 312 | "certs": {}, |
469 | | - "certs_acme": {}, |
| 313 | + "acme_certs": {}, |
470 | 314 | }, |
471 | 315 | } |
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 | + |
498 | 349 | return data |
0 commit comments