From e3d61c1310d16c818872c8794c1cbd6c8325e69c Mon Sep 17 00:00:00 2001 From: Omer Tuchfeld Date: Wed, 21 Feb 2024 11:43:27 +0100 Subject: [PATCH] Change a cluster's additional trust bundle # Main change Support changing a cluster's additional trust bundle. Changes all locations where the additional trust bundle is stored in. If an existing trust bundle is not found, this will cause an error, as creating the relevant resources is beyond the scope of this tool. The trust bundle's validity will not be checked. When using a RECERT_CONFIG file, raw PEMS can be used instead of a path to a trust bundle file. When using this feature it is recommended to also run the `update-ca-trust` script after running recert to ensure that the trust bundle is properly updated in all locations. # Other changes * Created `./hack/` directory to store some certs used during `./run_seed.sh` * Deprecated --static-files and --static-dirs, which were used for both recert and rename. Now `--crypto-dir` and `--crypto-file` will be used for recert while `--cluster-customization-dir` and `--cluster-customization-file` will be used for rename (aka cluster customization). This was needed because /etc/pki is full of certs we discover and fail to process during recert, but we do need to process /etc/pki for editing the additional trust bundle cluster customization. Using `--additional-trust-bundle` along with `--static-*` will cause an error. The old behavior for `--static-files` and `--static-dirs` is maintained for backwards compatibility, but they cannot be used along with the new flags. * Made ConfigPath a less leaky abstraction for ClioPath and moved its relevant code to its own module `path` (under `config`) * Renamed many `cli_parse` functions to `parse` as those functions were used outside of CLI parsing as well (during config file parsing) * Refactored config parsing into topical functions because that functions was getting a bit too long --- hack/dummy_trust_bundle.pem | 48 ++ hack/dummy_use_cert.crt | 17 + run_seed.sh | 85 ++- src/cluster_crypto/scanning.rs | 2 +- src/cnsanreplace.rs | 2 +- src/config.rs | 651 ++++++++++++------ src/config/cli.rs | 48 +- src/config/path.rs | 44 ++ src/ocp_postprocess.rs | 46 +- .../additional_trust_bundle.rs | 95 +++ .../additional_trust_bundle/etcd_rename.rs | 270 ++++++++ .../filesystem_rename.rs | 95 +++ .../additional_trust_bundle/utils.rs | 88 +++ src/ocp_postprocess/cluster_domain_rename.rs | 2 +- .../cluster_domain_rename/params.rs | 2 +- src/ocp_postprocess/fnv.rs | 9 + src/ocp_postprocess/go_base32.rs | 65 ++ src/ocp_postprocess/hostname_rename.rs | 2 +- src/ocp_postprocess/ip_rename.rs | 2 +- src/ocp_postprocess/pull_secret_rename.rs | 2 +- src/recert.rs | 16 +- src/use_cert.rs | 2 +- src/use_key.rs | 2 +- 23 files changed, 1322 insertions(+), 273 deletions(-) create mode 100644 hack/dummy_trust_bundle.pem create mode 100644 hack/dummy_use_cert.crt create mode 100644 src/config/path.rs create mode 100644 src/ocp_postprocess/additional_trust_bundle.rs create mode 100644 src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs create mode 100644 src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs create mode 100644 src/ocp_postprocess/additional_trust_bundle/utils.rs create mode 100644 src/ocp_postprocess/go_base32.rs diff --git a/hack/dummy_trust_bundle.pem b/hack/dummy_trust_bundle.pem new file mode 100644 index 00000000..177a60d0 --- /dev/null +++ b/hack/dummy_trust_bundle.pem @@ -0,0 +1,48 @@ +# Foo +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIUP+AxIkXJXTEhNGLH2qjmE6Gp0fowDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDIyNTlaFw0yNTAzMDEx +MDIyNTlaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa +BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC0wzg+7X2Amb5g60g0TstLgC0XnJRZq/YZUUsJMmm3qMb/+GYJ +AJzHxiycUfbRJtYvjx0SBmAX/kDRVCEQKcN5d/y3zeq709YO40kvouScfstsxM8l +PFLOmM8/Dqey1WblSJERBLbLherDnMwR7EMXkyZ/AfHUXmhVoIZE9ywsZpNcVW6Z +7x/+Izbj1s305vrxEkZDw6b3oMG5uooQgP5NZFXSamzJgviP0L/usvbRMtAWphoj +WhMeNuOdymLwRzm2l+2Qp/JDWktgHccmrbbi1c6pwhsIJBj4KOyb9zROTnYXyS/j +0b7GzVcffveV6E58rGa2ILyIsCv6gt8LgFnxAgMBAAGjUzBRMB0GA1UdDgQWBBQ5 +nh0SeZxZ969ps+9ywPEoOVasxTAfBgNVHSMEGDAWgBQ5nh0SeZxZ969ps+9ywPEo +OVasxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAWxsqfdm8h +AeNY8vPcRdtB9KYU5sZLs4NtlBFSdn+pHmeXZwwkjQDNhVcEMKdpZI4BpS11Ggwh +1d3BCCC/5M6yVqm+EMKJvA9VCeM8d1WJ1yVyXcgGuegf8kr3v+lr7Ll59qZGP5Ir +WwE8WRns7uFOCqYJCxo1VFXitZZuIugr3NUSimBPoJf1hDYdye3K3Q+grF2GyNII +5Yo+/VSR4ejIvJYAFp91Ycep7S0/+qhFpsjEG0Qw3Ly6WqQoCqdmIsyqFgWHsIlY +oJxV5wTX/c9DDZLR0VUD19aDV3B9kb7Cf+h7S4RsORWCyi7+58FKkkD6Ryc0I1K6 +xw3RWhfd9o1d +-----END CERTIFICATE----- + + + +# All +# the Bars +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIULnisjJLte3Vvt4o1f+5vSQg542cwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDI1MDFaFw0yNTAzMDEx +MDI1MDFaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa +BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC2dhK7xTnoTB3wN1l3NsLTp5YR0KFfBTjMcDgSzUy/GN79c2cF +JzSuiYUi7SCmFjn3soNqpXHFzCox6KIs9R6PL4epaQM76EVG/Xy6mdDvFnZvqypi +wmK6J0AGajOxItYUGb2a3Zmt/2nliW6t8sW/vhovHRu7YROo4uJygIp2UUFct2Lk +8C7XkJX5RXW+sKTiNddIjhmDFD0vHfvNvQ6AIayJTmXy272+aqYNJWB2wS/2uD3Z ++WOpiINetCtkASoiE7nzBQw+WsTfeFJH2TnI5pnSaHdLRUQtzoLO0/FgQ5WBfJg5 +aH03DLfQ9GEdzlsOkPOEgHXqDFMjTQCwcue3AgMBAAGjUzBRMB0GA1UdDgQWBBRd +0Zs+cm0gPHGKoQrerC18Pa3B3zAfBgNVHSMEGDAWgBRd0Zs+cm0gPHGKoQrerC18 +Pa3B3zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAepPrWqB9h +JkqtgJrP8SkQVulTVKYj66J5JxM5vZR96Z4UnbA3WNxezev0jMCYuV0twHPN8avs +Jern+/n7vgQ3ziiLVdtrN8PqK1X1apSurVmaiIw4tRcv5TVL5OD95sTyJh5bUBpM +DGtCTraPZxLIDKm9byunobXtJVcutw4oHKtFy/LlFWePCnvFzvx6ZFswLAXgxhf9 +EtjDf3v0cjDn9yRzjYFrwHiQ53A75YTwFyk21q7Gh1G0yspfBeq7cej2wK1PnfiC +42TI0UzcqRV4CWDoARMSV8yMLajZ0g1eEreUprwmFcOy17V7KCeV6E8lKb21OU8M +Ad9q3H0iXjct +-----END CERTIFICATE----- diff --git a/hack/dummy_use_cert.crt b/hack/dummy_use_cert.crt new file mode 100644 index 00000000..efbed8d2 --- /dev/null +++ b/hack/dummy_use_cert.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyzCCAbMCFAoie5EUqnUAHimqxbJBHV0MGVbwMA0GCSqGSIb3DQEBCwUAMCIx +IDAeBgNVBAMMF2FkbWluLWt1YmVjb25maWctc2lnbmVyMB4XDTI0MDEwOTEzMTky +NVoXDTI0MDIwODEzMTkyNVowIjEgMB4GA1UEAwwXYWRtaW4ta3ViZWNvbmZpZy1z +aWduZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2fz96uc8fDoNV +RaBB9iQ+i5Y76IZf0XOdGID8WVaqPlqH+NgLUaFa39T+78FhZW3794Lbeyu/PnYT +ufMyKnJEulVO7W7gPHaqWyuN08/m6SH5ycTEgUAXK1q1yVR/vM6HnV/UPUCfbDaW +RFOrUgGNwNywhEjqyzyUxJFixxS6Rk7JmouROD2ciNhBn6wNFByVHN9j4nQUOhXC +A0JjuiPH7ybvcHjmg3mKDJusyVq4pl0faahOxn0doILfXaHHwRxyEnP3V3arpPer +FvwlHh2Cfat+ijFPSD9pN3KmoeAviOHZVLQ/jKzkQvzlvva3mhEpLE5Zje1lMpvq +fjDheW9bAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAC7oi/Ht0lidcx6XvOBz6W1m +LU02e2yHuDzw6E3WuNoqAdPpleFRV4mLDnv8mEavH5sje0L5veHtOq3Ny4pc06B+ +ETB2aCW4GQ4mPvN9Jyi6sxLQQaVLpFrtPPB08NawNbbcYWUrAihO1uIXLhaCYZWw +H3aWlqRvGECazYZIPcFoV20jygrcwMhixSZjYyHhJN0LYO5sjiKcMnI8EkHuqE17 +7CPogicZte+m49Mo+f7b8asmKBSafdTUSVAt9Q3Fc3PTJSMW5lxfx1vIR/og33WJ +BgIejfD1dYW2Fp02z5sF6Pw6vhobpfDYgsTAKNonh5P6NxMiD14eQxYrNJ6DAF0= +-----END CERTIFICATE----- diff --git a/run_seed.sh b/run_seed.sh index 2c3941b5..95e3e498 100755 --- a/run_seed.sh +++ b/run_seed.sh @@ -70,11 +70,18 @@ if [[ -n "$WITH_CONFIG" ]]; then RECERT_CONFIG=<(echo ' dry_run: false etcd_endpoint: localhost:2379 -static_dirs: +crypto_dirs: - backup/etc/kubernetes - backup/var/lib/kubelet - backup/etc/machine-config-daemon -static_files: +crypto_files: +- backup/etc/mcs-machine-config-content.json +cluster_customization_dirs: +- backup/etc/kubernetes +- backup/var/lib/kubelet +- backup/etc/machine-config-daemon +- backup/etc/pki/ca-trust +cluster_customization_files: - backup/etc/mcs-machine-config-content.json cn_san_replace_rules: - api-int.seed.redhat.com:api-int.new-name.foo.com @@ -104,6 +111,55 @@ cluster_rename: new-name:foo.com:some-random-infra-id hostname: test.hostname ip: 192.168.126.99 kubeadmin_password_hash: "$2a$10$20Q4iRLy7cWZkjn/D07bF.RZQZonKwstyRGH0qiYbYRkx5Pe4Ztyi" +additional_trust_bundle: | + # Foo + -----BEGIN CERTIFICATE----- + MIIDZTCCAk2gAwIBAgIUP+AxIkXJXTEhNGLH2qjmE6Gp0fowDQYJKoZIhvcNAQEL + BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE + CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDIyNTlaFw0yNTAzMDEx + MDIyNTlaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa + BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB + DwAwggEKAoIBAQC0wzg+7X2Amb5g60g0TstLgC0XnJRZq/YZUUsJMmm3qMb/+GYJ + AJzHxiycUfbRJtYvjx0SBmAX/kDRVCEQKcN5d/y3zeq709YO40kvouScfstsxM8l + PFLOmM8/Dqey1WblSJERBLbLherDnMwR7EMXkyZ/AfHUXmhVoIZE9ywsZpNcVW6Z + 7x/+Izbj1s305vrxEkZDw6b3oMG5uooQgP5NZFXSamzJgviP0L/usvbRMtAWphoj + WhMeNuOdymLwRzm2l+2Qp/JDWktgHccmrbbi1c6pwhsIJBj4KOyb9zROTnYXyS/j + 0b7GzVcffveV6E58rGa2ILyIsCv6gt8LgFnxAgMBAAGjUzBRMB0GA1UdDgQWBBQ5 + nh0SeZxZ969ps+9ywPEoOVasxTAfBgNVHSMEGDAWgBQ5nh0SeZxZ969ps+9ywPEo + OVasxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAWxsqfdm8h + AeNY8vPcRdtB9KYU5sZLs4NtlBFSdn+pHmeXZwwkjQDNhVcEMKdpZI4BpS11Ggwh + 1d3BCCC/5M6yVqm+EMKJvA9VCeM8d1WJ1yVyXcgGuegf8kr3v+lr7Ll59qZGP5Ir + WwE8WRns7uFOCqYJCxo1VFXitZZuIugr3NUSimBPoJf1hDYdye3K3Q+grF2GyNII + 5Yo+/VSR4ejIvJYAFp91Ycep7S0/+qhFpsjEG0Qw3Ly6WqQoCqdmIsyqFgWHsIlY + oJxV5wTX/c9DDZLR0VUD19aDV3B9kb7Cf+h7S4RsORWCyi7+58FKkkD6Ryc0I1K6 + xw3RWhfd9o1d + -----END CERTIFICATE----- + + + + # All + # the Bars + -----BEGIN CERTIFICATE----- + MIIDZTCCAk2gAwIBAgIULnisjJLte3Vvt4o1f+5vSQg542cwDQYJKoZIhvcNAQEL + BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE + CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDI1MDFaFw0yNTAzMDEx + MDI1MDFaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa + BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB + DwAwggEKAoIBAQC2dhK7xTnoTB3wN1l3NsLTp5YR0KFfBTjMcDgSzUy/GN79c2cF + JzSuiYUi7SCmFjn3soNqpXHFzCox6KIs9R6PL4epaQM76EVG/Xy6mdDvFnZvqypi + wmK6J0AGajOxItYUGb2a3Zmt/2nliW6t8sW/vhovHRu7YROo4uJygIp2UUFct2Lk + 8C7XkJX5RXW+sKTiNddIjhmDFD0vHfvNvQ6AIayJTmXy272+aqYNJWB2wS/2uD3Z + +WOpiINetCtkASoiE7nzBQw+WsTfeFJH2TnI5pnSaHdLRUQtzoLO0/FgQ5WBfJg5 + aH03DLfQ9GEdzlsOkPOEgHXqDFMjTQCwcue3AgMBAAGjUzBRMB0GA1UdDgQWBBRd + 0Zs+cm0gPHGKoQrerC18Pa3B3zAfBgNVHSMEGDAWgBRd0Zs+cm0gPHGKoQrerC18 + Pa3B3zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAepPrWqB9h + JkqtgJrP8SkQVulTVKYj66J5JxM5vZR96Z4UnbA3WNxezev0jMCYuV0twHPN8avs + Jern+/n7vgQ3ziiLVdtrN8PqK1X1apSurVmaiIw4tRcv5TVL5OD95sTyJh5bUBpM + DGtCTraPZxLIDKm9byunobXtJVcutw4oHKtFy/LlFWePCnvFzvx6ZFswLAXgxhf9 + EtjDf3v0cjDn9yRzjYFrwHiQ53A75YTwFyk21q7Gh1G0yspfBeq7cej2wK1PnfiC + 42TI0UzcqRV4CWDoARMSV8yMLajZ0g1eEreUprwmFcOy17V7KCeV6E8lKb21OU8M + Ad9q3H0iXjct + -----END CERTIFICATE----- summary_file: summary.yaml summary_file_clean: summary_redacted.yaml extend_expiration: true @@ -113,23 +169,36 @@ threads: 1 ') cargo run --release else # shellcheck disable=2016 - cargo run --release -- \ + cargo run -- \ --etcd-endpoint localhost:2379 \ - --static-dir backup/etc/kubernetes \ - --static-dir backup/var/lib/kubelet \ - --static-dir backup/etc/machine-config-daemon \ - --static-file backup/etc/mcs-machine-config-content.json \ + \ + --crypto-dir backup/etc/kubernetes \ + --crypto-dir backup/var/lib/kubelet \ + --crypto-dir backup/etc/machine-config-daemon \ + --crypto-file backup/etc/mcs-machine-config-content.json \ + \ + --cluster-customization-dir backup/etc/kubernetes \ + --cluster-customization-dir backup/var/lib/kubelet \ + --cluster-customization-dir backup/etc/machine-config-daemon \ + --cluster-customization-dir backup/etc/pki/ca-trust \ + --cluster-customization-file backup/etc/mcs-machine-config-content.json \ + \ --cn-san-replace api-int.seed.redhat.com:api-int.new-name.foo.com \ --cn-san-replace api.seed.redhat.com:api.new-name.foo.com \ --cn-san-replace *.apps.seed.redhat.com:*.apps.new-name.foo.com \ --cn-san-replace 192.168.126.10:192.168.127.11 \ + --use-cert ./hack/dummy_use_cert.crt \ + \ --cluster-rename new-name:foo.com:some-random-infra-id \ --hostname test.hostname \ --ip 192.168.126.99 \ --kubeadmin-password-hash '$2a$10$20Q4iRLy7cWZkjn/D07bF.RZQZonKwstyRGH0qiYbYRkx5Pe4Ztyi' \ + --additional-trust-bundle ./hack/dummy_trust_bundle.pem \ + --pull-secret '{"auths":{"empty_registry":{"username":"empty","password":"empty","auth":"ZW1wdHk6ZW1wdHk=","email":""}}}' \ + \ --summary-file summary.yaml \ --summary-file-clean summary_redacted.yaml \ - --pull-secret '{"auths":{"empty_registry":{"username":"empty","password":"empty","auth":"ZW1wdHk6ZW1wdHk=","email":""}}}' \ + \ --extend-expiration # --regenerate-server-ssh-keys backup/etc/ssh/ \ fi diff --git a/src/cluster_crypto/scanning.rs b/src/cluster_crypto/scanning.rs index c6e26dff..9081aef7 100644 --- a/src/cluster_crypto/scanning.rs +++ b/src/cluster_crypto/scanning.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ cluster_crypto::{crypto_objects::process_unknown_value, json_crawl}, - config::ConfigPath, + config::path::ConfigPath, file_utils::{self, read_file_to_string}, k8s_etcd::InMemoryK8sEtcd, recert::timing::RunTime, diff --git a/src/cnsanreplace.rs b/src/cnsanreplace.rs index e408c21d..57cdd02f 100644 --- a/src/cnsanreplace.rs +++ b/src/cnsanreplace.rs @@ -16,7 +16,7 @@ impl std::fmt::Display for CnSanReplace { } impl CnSanReplace { - pub(crate) fn cli_parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { // Also allow comma separation to support IPv6 let split = if value.contains(',') { value.split(',') } else { value.split(':') }.collect::>(); diff --git a/src/config.rs b/src/config.rs index fc9b1710..3d11dd61 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ -use std::{env, ops::Deref, path::Path, sync::atomic::Ordering::Relaxed}; - +use self::{cli::Cli, path::ConfigPath}; use crate::{ cluster_crypto::REDACT_SECRETS, cnsanreplace::{CnSanReplace, CnSanReplaceRules}, @@ -9,47 +8,19 @@ use crate::{ }; use anyhow::{ensure, Context, Result}; use clap::Parser; -use clio::ClioPath; use itertools::Itertools; use serde::Serialize; use serde_json::Value; - -use self::cli::Cli; +use std::{env, path::PathBuf, sync::atomic::Ordering::Relaxed}; mod cli; - -#[derive(Clone, Debug)] -pub(crate) struct ConfigPath(pub(crate) ClioPath); - -impl AsRef for ConfigPath { - fn as_ref(&self) -> &ClioPath { - &self.0 - } -} - -impl Deref for ConfigPath { - type Target = Path; - - fn deref(&self) -> &Self::Target { - self.0.path() - } -} - -impl From for ConfigPath { - fn from(clio_path: ClioPath) -> Self { - Self(clio_path) - } -} - -impl serde::Serialize for ConfigPath { - fn serialize(&self, serializer: S) -> std::result::Result { - serializer.serialize_str(self.0.to_string_lossy().as_ref()) - } -} +pub(crate) mod path; /// All the user requested customizations, coalesced into a single struct for convenience #[derive(serde::Serialize)] pub(crate) struct CryptoCustomizations { + pub(crate) dirs: Vec, + pub(crate) files: Vec, pub(crate) cn_san_replace_rules: CnSanReplaceRules, pub(crate) use_key_rules: UseKeyRules, pub(crate) use_cert_rules: UseCertRules, @@ -59,11 +30,14 @@ pub(crate) struct CryptoCustomizations { #[derive(serde::Serialize)] pub(crate) struct ClusterCustomizations { + pub(crate) dirs: Vec, + pub(crate) files: Vec, pub(crate) cluster_rename: Option, pub(crate) hostname: Option, pub(crate) ip: Option, pub(crate) kubeadmin_password_hash: Option, pub(crate) pull_secret: Option, + pub(crate) additional_trust_bundle: Option, } /// All parsed CLI arguments, coalesced into a single struct for convenience @@ -71,8 +45,6 @@ pub(crate) struct ClusterCustomizations { pub(crate) struct RecertConfig { pub(crate) dry_run: bool, pub(crate) etcd_endpoint: Option, - pub(crate) static_dirs: Vec, - pub(crate) static_files: Vec, pub(crate) crypto_customizations: CryptoCustomizations, pub(crate) cluster_customizations: ClusterCustomizations, pub(crate) threads: Option, @@ -131,178 +103,16 @@ impl RecertConfig { let mut value = value.as_object().context("config file must be a YAML object")?.clone(); - let dry_run = value - .remove("dry_run") - .unwrap_or(Value::Bool(false)) - .as_bool() - .context("dry_run must be a boolean")?; + let (crypto_dirs, crypto_files, cluster_customization_dirs, cluster_customization_files) = parse_dir_file_config(&mut value)?; - let etcd_endpoint = match value.remove("etcd_endpoint") { - Some(value) => Some(value.as_str().context("etcd_endpoint must be a string")?.to_string()), - None => None, - }; + let (cn_san_replace_rules, use_key_rules, use_cert_rules, extend_expiration, force_expire) = + parse_crypto_customization_config(&mut value)?; - let static_dirs = match value.remove("static_dirs") { - Some(value) => value - .as_array() - .context("static_dirs must be an array")? - .iter() - .map(|value| { - let clio_path = ClioPath::new(value.as_str().context("static_dirs must be an array of strings")?) - .context(format!("config dir {}", value.as_str().unwrap()))?; - - ensure!(clio_path.try_exists()?, format!("static_dir must exist: {}", clio_path)); - ensure!(clio_path.is_dir(), format!("static_dir must be a directory: {}", clio_path)); - - Ok(ConfigPath::from(clio_path)) - }) - .collect::>>()?, - None => vec![], - }; - - let static_files = match value.remove("static_files") { - Some(value) => value - .as_array() - .context("static_files must be an array")? - .iter() - .map(|value| { - let clio_path = ClioPath::new(value.as_str().context("static_files must be an array of strings")?) - .context(format!("config file {}", value.as_str().unwrap()))?; + let (cluster_rename, hostname, ip, pull_secret, set_kubeadmin_password_hash, additional_trust_bundle) = + parse_cluster_customization_config(&mut value)?; - ensure!(clio_path.try_exists()?, format!("static_file must exist: {}", clio_path)); - ensure!(clio_path.is_file(), format!("static_file must be a file: {}", clio_path)); - - Ok(ConfigPath::from(clio_path)) - }) - .collect::>>()?, - None => vec![], - }; - - let cn_san_replace_rules = match value.remove("cn_san_replace_rules") { - Some(value) => CnSanReplaceRules( - value - .as_array() - .context("cn_san_replace_rules must be an array")? - .iter() - .map(|value| { - CnSanReplace::cli_parse(value.as_str().context("cn_san_replace_rules must be an array of strings")?) - .context(format!("cn_san_replace_rule {}", value.as_str().unwrap())) - }) - .collect::>>()?, - ), - None => CnSanReplaceRules(vec![]), - }; - - let use_key_rules = match value.remove("use_key_rules") { - Some(value) => UseKeyRules( - value - .as_array() - .context("use_key_rules must be an array")? - .iter() - .map(|value| { - UseKey::cli_parse(value.as_str().context("use_key_rules must be an array of strings")?) - .context(format!("use_key_rule {}", value.as_str().unwrap())) - }) - .collect::>>()?, - ), - None => UseKeyRules(vec![]), - }; - - let use_cert_rules = match value.remove("use_cert_rules") { - Some(value) => UseCertRules( - value - .as_array() - .context("use_cert_rules must be an array")? - .iter() - .map(|value| { - UseCert::cli_parse(value.as_str().context("use_cert_rules must be an array of strings")?) - .context(format!("use_cert_rule {}", value.as_str().unwrap())) - }) - .collect::>>()?, - ), - None => UseCertRules(vec![]), - }; - - let extend_expiration = value - .remove("extend_expiration") - .unwrap_or(Value::Bool(false)) - .as_bool() - .context("extend_expiration must be a boolean")?; - - let force_expire = value - .remove("force_expire") - .unwrap_or(Value::Bool(false)) - .as_bool() - .context("force_expire must be a boolean")?; - - let cluster_rename = match value.remove("cluster_rename") { - Some(value) => Some( - ClusterNamesRename::cli_parse(value.as_str().context("cluster_rename must be a string")?) - .context(format!("cluster_rename {}", value.as_str().unwrap()))?, - ), - None => None, - }; - - let hostname = match value.remove("hostname") { - Some(value) => Some(value.as_str().context("hostname must be a string")?.to_string()), - None => None, - }; - - let ip = match value.remove("ip") { - Some(value) => Some(value.as_str().context("ip must be a string")?.to_string()), - None => None, - }; - - let pull_secret = match value.remove("pull_secret") { - Some(value) => Some(value.as_str().context("pull_secret must be a string")?.to_string()), - None => None, - }; - - let set_kubeadmin_password_hash = match value.remove("kubeadmin_password_hash") { - Some(value) => Some(value.as_str().context("set_kubeadmin_password_hash must be a string")?.to_string()), - None => None, - }; - - let threads = match value.remove("threads") { - Some(value) => Some( - value - .as_u64() - .context("threads must be an integer")? - .try_into() - .context("threads must be an integer")?, - ), - None => None, - }; - - let regenerate_server_ssh_keys = match value.remove("regenerate_server_ssh_keys") { - Some(value) => { - let clio_path = ConfigPath::from( - ClioPath::new(value.as_str().context("regenerate_server_ssh_keys must be a string")?) - .context(format!("regenerate_server_ssh_keys {}", value.as_str().unwrap()))?, - ); - - ensure!(clio_path.try_exists()?, "regenerate_server_ssh_keys must exist"); - ensure!(clio_path.is_dir(), "regenerate_server_ssh_keys must be a directory"); - Some(clio_path) - } - None => None, - }; - - let summary_file = match value.remove("summary_file") { - Some(value) => Some(ConfigPath::from( - ClioPath::new(value.as_str().context("summary_file must be a string")?) - .context(format!("summary_file {}", value.as_str().unwrap()))?, - )), - None => None, - }; - - let summary_file_clean = match value.remove("summary_file_clean") { - Some(value) => Some(ConfigPath::from( - ClioPath::new(value.as_str().context("summary_file_clean must be a string")?) - .context(format!("summary_file_clean {}", value.as_str().unwrap()))?, - )), - None => None, - }; + let (dry_run, etcd_endpoint, threads, regenerate_server_ssh_keys, summary_file, summary_file_clean) = + parse_misc_config(&mut value)?; ensure!( value.is_empty(), @@ -310,25 +120,32 @@ impl RecertConfig { value.keys().map(|key| key.to_string()).join(", ") ); + let crypto_customizations = CryptoCustomizations { + dirs: crypto_dirs, + files: crypto_files, + cn_san_replace_rules, + use_key_rules, + use_cert_rules, + extend_expiration, + force_expire, + }; + + let cluster_customizations = ClusterCustomizations { + dirs: cluster_customization_dirs, + files: cluster_customization_files, + cluster_rename, + hostname, + ip, + kubeadmin_password_hash: set_kubeadmin_password_hash, + pull_secret, + additional_trust_bundle, + }; + let recert_config = Self { dry_run, etcd_endpoint, - static_dirs, - static_files, - crypto_customizations: CryptoCustomizations { - cn_san_replace_rules, - use_key_rules, - use_cert_rules, - extend_expiration, - force_expire, - }, - cluster_customizations: ClusterCustomizations { - cluster_rename, - hostname, - ip, - kubeadmin_password_hash: set_kubeadmin_password_hash, - pull_secret, - }, + crypto_customizations, + cluster_customizations, threads, regenerate_server_ssh_keys, summary_file, @@ -360,9 +177,17 @@ impl RecertConfig { Ok(Self { dry_run: cli.dry_run, etcd_endpoint: cli.etcd_endpoint, - static_dirs: cli.static_dir.into_iter().map(ConfigPath::from).collect(), - static_files: cli.static_file.into_iter().map(ConfigPath::from).collect(), crypto_customizations: CryptoCustomizations { + dirs: if cli.static_dir.is_empty() { + cli.crypto_dir.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_dir.clone().into_iter().map(ConfigPath::from).collect() + }, + files: if cli.static_file.is_empty() { + cli.crypto_file.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_file.clone().into_iter().map(ConfigPath::from).collect() + }, cn_san_replace_rules: CnSanReplaceRules(cli.cn_san_replace), use_key_rules: UseKeyRules(cli.use_key), use_cert_rules: UseCertRules(cli.use_cert), @@ -370,11 +195,22 @@ impl RecertConfig { force_expire: cli.force_expire, }, cluster_customizations: ClusterCustomizations { + dirs: if cli.static_dir.is_empty() { + cli.cluster_customization_dir.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_dir.into_iter().map(ConfigPath::from).collect() + }, + files: if cli.static_file.is_empty() { + cli.cluster_customization_file.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_file.into_iter().map(ConfigPath::from).collect() + }, cluster_rename: cli.cluster_rename, hostname: cli.hostname, ip: cli.ip, kubeadmin_password_hash: cli.kubeadmin_password_hash, pull_secret: cli.pull_secret, + additional_trust_bundle: cli.additional_trust_bundle, }, threads: cli.threads, regenerate_server_ssh_keys: cli.regenerate_server_ssh_keys.map(ConfigPath::from), @@ -403,3 +239,372 @@ impl RecertConfig { }) } } + +#[allow(clippy::type_complexity)] +fn parse_misc_config( + value: &mut serde_json::Map, +) -> Result<( + bool, + Option, + Option, + Option, + Option, + Option, +)> { + let dry_run = value + .remove("dry_run") + .unwrap_or(Value::Bool(false)) + .as_bool() + .context("dry_run must be a boolean")?; + let etcd_endpoint = match value.remove("etcd_endpoint") { + Some(value) => Some(value.as_str().context("etcd_endpoint must be a string")?.to_string()), + None => None, + }; + let threads = match value.remove("threads") { + Some(value) => Some( + value + .as_u64() + .context("threads must be an integer")? + .try_into() + .context("threads must be an integer")?, + ), + None => None, + }; + let regenerate_server_ssh_keys = match value.remove("regenerate_server_ssh_keys") { + Some(value) => { + let config_path = ConfigPath::new(value.as_str().context("regenerate_server_ssh_keys must be a string")?) + .context(format!("regenerate_server_ssh_keys {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, "regenerate_server_ssh_keys must exist"); + ensure!(config_path.is_dir(), "regenerate_server_ssh_keys must be a directory"); + Some(config_path) + } + None => None, + }; + let summary_file = match value.remove("summary_file") { + Some(value) => Some( + ConfigPath::new(value.as_str().context("summary_file must be a string")?) + .context(format!("summary_file {}", value.as_str().unwrap()))?, + ), + None => None, + }; + let summary_file_clean = match value.remove("summary_file_clean") { + Some(value) => Some( + ConfigPath::new(value.as_str().context("summary_file_clean must be a string")?) + .context(format!("summary_file_clean {}", value.as_str().unwrap()))?, + ), + None => None, + }; + Ok(( + dry_run, + etcd_endpoint, + threads, + regenerate_server_ssh_keys, + summary_file, + summary_file_clean, + )) +} + +#[allow(clippy::type_complexity)] +fn parse_cluster_customization_config( + value: &mut serde_json::Map, +) -> Result<( + Option, + Option, + Option, + Option, + Option, + Option, +)> { + let cluster_rename = match value.remove("cluster_rename") { + Some(value) => Some( + ClusterNamesRename::parse(value.as_str().context("cluster_rename must be a string")?) + .context(format!("cluster_rename {}", value.as_str().unwrap()))?, + ), + None => None, + }; + let hostname = match value.remove("hostname") { + Some(value) => Some(value.as_str().context("hostname must be a string")?.to_string()), + None => None, + }; + let ip = match value.remove("ip") { + Some(value) => Some(value.as_str().context("ip must be a string")?.to_string()), + None => None, + }; + let pull_secret = match value.remove("pull_secret") { + Some(value) => Some(value.as_str().context("pull_secret must be a string")?.to_string()), + None => None, + }; + let set_kubeadmin_password_hash = match value.remove("kubeadmin_password_hash") { + Some(value) => Some(value.as_str().context("set_kubeadmin_password_hash must be a string")?.to_string()), + None => None, + }; + let additional_trust_bundle = match value.remove("additional_trust_bundle") { + Some(value) => Some(parse_additional_trust_bundle( + value.as_str().context("additional_trust_bundle must be a string")?, + )?), + None => None, + }; + Ok(( + cluster_rename, + hostname, + ip, + pull_secret, + set_kubeadmin_password_hash, + additional_trust_bundle, + )) +} + +fn parse_crypto_customization_config( + value: &mut serde_json::Map, +) -> Result<(CnSanReplaceRules, UseKeyRules, UseCertRules, bool, bool)> { + let cn_san_replace_rules = match value.remove("cn_san_replace_rules") { + Some(value) => CnSanReplaceRules( + value + .as_array() + .context("cn_san_replace_rules must be an array")? + .iter() + .map(|value| { + CnSanReplace::parse(value.as_str().context("cn_san_replace_rules must be an array of strings")?) + .context(format!("cn_san_replace_rule {}", value.as_str().unwrap())) + }) + .collect::>>()?, + ), + None => CnSanReplaceRules(vec![]), + }; + let use_key_rules = match value.remove("use_key_rules") { + Some(value) => UseKeyRules( + value + .as_array() + .context("use_key_rules must be an array")? + .iter() + .map(|value| { + UseKey::parse(value.as_str().context("use_key_rules must be an array of strings")?) + .context(format!("use_key_rule {}", value.as_str().unwrap())) + }) + .collect::>>()?, + ), + None => UseKeyRules(vec![]), + }; + let use_cert_rules = match value.remove("use_cert_rules") { + Some(value) => UseCertRules( + value + .as_array() + .context("use_cert_rules must be an array")? + .iter() + .map(|value| { + UseCert::parse(value.as_str().context("use_cert_rules must be an array of strings")?) + .context(format!("use_cert_rule {}", value.as_str().unwrap())) + }) + .collect::>>()?, + ), + None => UseCertRules(vec![]), + }; + let extend_expiration = value + .remove("extend_expiration") + .unwrap_or(Value::Bool(false)) + .as_bool() + .context("extend_expiration must be a boolean")?; + let force_expire = value + .remove("force_expire") + .unwrap_or(Value::Bool(false)) + .as_bool() + .context("force_expire must be a boolean")?; + Ok((cn_san_replace_rules, use_key_rules, use_cert_rules, extend_expiration, force_expire)) +} + +#[allow(clippy::type_complexity)] +fn parse_dir_file_config( + value: &mut serde_json::Map, +) -> Result<(Vec, Vec, Vec, Vec)> { + let static_dirs = match value.remove("static_dirs") { + Some(value) => { + ensure!( + value.get("crypto_dirs").is_none(), + "static_dirs and crypto_dirs are mutually exclusive" + ); + ensure!( + value.get("cluster_customization_dirs").is_none(), + "static_dirs and cluster_customization_dirs are mutually exclusive" + ); + ensure!( + value.get("additional_trust_bundle").is_none(), + "static_dirs and cluster_customization_dirs are mutually exclusive" + ); + + value + .as_array() + .context("static_dirs must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("static_dirs must be an array of strings")?) + .context(format!("config dir {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("static_dir must exist: {}", config_path)); + ensure!(config_path.is_dir(), format!("static_dir must be a directory: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()? + } + None => vec![], + }; + let static_files = match value.remove("static_files") { + Some(value) => { + ensure!( + value.get("crypto_files").is_none(), + "static_files and crypto_files are mutually exclusive" + ); + ensure!( + value.get("cluster_customization_files").is_none(), + "static_files and cluster_customization_files are mutually exclusive" + ); + ensure!( + value.get("additional_trust_bundle").is_none(), + "static_files and cluster_customization_files are mutually exclusive" + ); + + value + .as_array() + .context("static_files must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("static_files must be an array of strings")?) + .context(format!("config file {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("static_file must exist: {}", config_path)); + ensure!(config_path.is_file(), format!("static_file must be a file: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()? + } + None => vec![], + }; + let crypto_dirs = if static_dirs.is_empty() { + match value.remove("crypto_dirs") { + Some(value) => value + .as_array() + .context("crypto_dirs must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("crypto_dirs must be an array of strings")?) + .context(format!("crypto dir {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("crypto_dir must exist: {}", config_path)); + ensure!(config_path.is_dir(), format!("crypto_dir must be a directory: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_dirs.clone() + }; + let crypto_files = if static_files.is_empty() { + match value.remove("crypto_files") { + Some(value) => value + .as_array() + .context("crypto_files must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("crypto_files must be an array of strings")?) + .context(format!("crypto file {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("crypto_file must exist: {}", config_path)); + ensure!(config_path.is_file(), format!("crypto_file must be a file: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_files.clone() + }; + + let cluster_customization_dirs = if static_dirs.is_empty() { + match value.remove("cluster_customization_dirs") { + Some(value) => value + .as_array() + .context("cluster_customization_dirs must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("cluster_customization_dirs must be an array of strings")?) + .context(format!("cluster_customization dir {}", value.as_str().unwrap()))?; + + ensure!( + config_path.try_exists()?, + format!("cluster_customization_dir must exist: {}", config_path) + ); + ensure!( + config_path.is_dir(), + format!("cluster_customization_dir must be a directory: {}", config_path) + ); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_dirs + }; + + let cluster_customization_files = if static_files.is_empty() { + match value.remove("cluster_customization_files") { + Some(value) => value + .as_array() + .context("cluster_customization_files must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("cluster_customization_files must be an array of strings")?) + .context(format!("cluster_customization file {}", value.as_str().unwrap()))?; + + ensure!( + config_path.try_exists()?, + format!("cluster_customization_file must exist: {}", config_path) + ); + ensure!( + config_path.is_file(), + format!("cluster_customization_file must be a file: {}", config_path) + ); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_files + }; + Ok((crypto_dirs, crypto_files, cluster_customization_dirs, cluster_customization_files)) +} + +pub(crate) fn parse_additional_trust_bundle(value: &str) -> Result { + let bundle = if !value.contains('\n') { + let path = PathBuf::from(&value); + + ensure!(path.try_exists()?, "additional_trust_bundle must exist"); + ensure!(path.is_file(), "additional_trust_bundle must be a file"); + + String::from_utf8(std::fs::read(&path).context("failed to read additional_trust_bundle")?) + .context("additional_trust_bundle must be valid UTF-8")? + } else { + value.to_string() + }; + + let pems = pem::parse_many(bundle.as_bytes()).context("additional_trust_bundle must be valid PEM")?; + + ensure!(!pems.is_empty(), "additional_trust_bundle must contain at least one certificate"); + + ensure!( + pems.iter().all(|pem| pem.tag() == "CERTIFICATE"), + "additional_trust_bundle must contain only certificates" + ); + + // After parsing, we still return the raw bundle, as OpenShift also preserves the original + // comments and whitespace in the user's additional trust bundle + Ok(bundle) +} diff --git a/src/config/cli.rs b/src/config/cli.rs index b8203d2c..b5a41cb9 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -12,27 +12,49 @@ pub(crate) struct Cli { #[clap(long)] pub(crate) etcd_endpoint: Option, + // DEPRECATED: Use --crypto-dir and --cluster-customization-dir instead. This option will be + // removed in a future release. Cannot be used with --crypto-dir or --cluster-customization-dir + // or --additional-trust-bundle + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), groups = &["crypto_dir_paths", "cluster_customization_dir_paths", "adt_dirs"])] + pub(crate) static_dir: Vec, + + /// DEPRECATED: Use --crypto-file and --cluster-customization-file instead. This option will be + /// removed in a future release. Cannot be used with --crypto-file or + /// --cluster-customization-file or --additional-trust-bundle + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file(), groups = &["crypto_file_paths", "cluster_customization_file_paths", "adt_files"])] + pub(crate) static_file: Vec, + /// Directory to recertify, such as /var/lib/kubelet, /etc/kubernetes and /// /etc/machine-config-daemon. Can specify multiple times - #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir())] - pub(crate) static_dir: Vec, + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), group = "crypto_dir_paths")] + pub(crate) crypto_dir: Vec, /// A file to recertify, such as /etc/mcs-machine-config-content.json. Can specify multiple /// times - #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file())] - pub(crate) static_file: Vec, + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file(), group = "crypto_file_paths")] + pub(crate) crypto_file: Vec, + + /// Directory containing files involved in cluster customization, such as /var/lib/kubelet, + /// /etc/kubernetes, /etc/pki/ca-trust, etc. Can specify multiple. + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), group = "cluster_customization_dir_paths")] + pub(crate) cluster_customization_dir: Vec, + + /// File involved in cluster customization, such as /etc/mcs-machine-config-content.json. Can + /// specify multiple. + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file(), group = "cluster_customization_file_paths")] + pub(crate) cluster_customization_file: Vec, /// A list of strings to replace in the subject name of all certificates. Can specify multiple. /// --cn-san-replace foo:bar --cn-san-replace baz:qux will replace all instances of "foo" with /// "bar" and all instances of "baz" with "qux" in the CN/SAN of all certificates. - #[clap(long, value_parser = CnSanReplace::cli_parse)] + #[clap(long, value_parser = CnSanReplace::parse)] pub(crate) cn_san_replace: Vec, /// Experimental feature. Colon separated cluster name and cluster base domain. If given, many /// cluster resources which refer to a cluster name / cluster base domain (typically through /// URLs which they happen to contian) will be modified to use this cluster name and base /// domain instead. - #[clap(long, value_parser = ClusterNamesRename::cli_parse)] + #[clap(long, value_parser = ClusterNamesRename::parse)] pub(crate) cluster_rename: Option, /// If given, the cluster resources that include the hostname will be modified to use this one @@ -63,6 +85,16 @@ pub(crate) struct Cli { #[clap(long)] pub(crate) pull_secret: Option, + /// Change a cluster's trust bundle. Changes all locations where the trust bundle is stored in + /// the cluster. If an existing trust bundle is not found, this will cause an error, as + /// creating the relevant resources is beyond the scope of this tool. The trust bundle's + /// validity will not be checked. When using a RECERT_CONFIG file, raw PEMS can be used instead + /// of paths to trust bundle files. When using this option it is recommended to also run + /// update-ca-trust after running recert to ensure that the trust bundle is properly updated in + /// all locations. + #[clap(long, value_parser = super::parse_additional_trust_bundle, groups = &["adt_dirs", "adt_files"])] + pub(crate) additional_trust_bundle: Option, + /// A list of CNs and the private keys to use for their certs. By default, new keys will be /// generated for all regenerated certificates, this option allows you to use existing keys /// instead. Must come in pairs of CN and private key file path, separated by a space. For @@ -72,7 +104,7 @@ pub(crate) struct Cli { /// regenerated. /// /// When using a RECERT_CONFIG file, raw PEMS can be used instead of paths to key files. - #[clap(long, value_parser = UseKey::cli_parse)] + #[clap(long, value_parser = UseKey::parse)] pub(crate) use_key: Vec, /// Same as --use-key, but for when a cert needs to be replaced in its entirety, rather than @@ -83,7 +115,7 @@ pub(crate) struct Cli { /// the --extend-expiration flag is used. /// /// When using a RECERT_CONFIG file, raw PEMS can be used instead of paths to cert files. - #[clap(long, value_parser = UseCert::cli_parse)] + #[clap(long, value_parser = UseCert::parse)] pub(crate) use_cert: Vec, /// Extend expiration of all certificates to (original_expiration + (now - issue date)), and diff --git a/src/config/path.rs b/src/config/path.rs new file mode 100644 index 00000000..26437196 --- /dev/null +++ b/src/config/path.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use clio::ClioPath; +use std::{ops::Deref, path::Path}; + +#[derive(Clone, Debug)] +pub(crate) struct ConfigPath(pub(crate) ClioPath); + +impl std::fmt::Display for ConfigPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_string_lossy().fmt(f) + } +} + +impl AsRef for ConfigPath { + fn as_ref(&self) -> &ClioPath { + &self.0 + } +} + +impl Deref for ConfigPath { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.0.path() + } +} + +impl From for ConfigPath { + fn from(clio_path: ClioPath) -> Self { + Self(clio_path) + } +} + +impl ConfigPath { + pub(crate) fn new(path: &str) -> Result { + Ok(Self(ClioPath::new(path)?)) + } +} + +impl serde::Serialize for ConfigPath { + fn serialize(&self, serializer: S) -> std::result::Result { + serializer.serialize_str(self.0.to_string_lossy().as_ref()) + } +} diff --git a/src/ocp_postprocess.rs b/src/ocp_postprocess.rs index a1da9543..ef8c028a 100644 --- a/src/ocp_postprocess.rs +++ b/src/ocp_postprocess.rs @@ -1,7 +1,7 @@ use self::cluster_domain_rename::params::ClusterNamesRename; use crate::{ cluster_crypto::locations::K8sResourceLocation, - config::{ClusterCustomizations, ConfigPath}, + config::{path::ConfigPath, ClusterCustomizations}, file_utils::{self, read_file_to_string}, k8s_etcd::{self, get_etcd_json, put_etcd_yaml}, }; @@ -15,8 +15,10 @@ use k8s_etcd::InMemoryK8sEtcd; use sha2::Digest; use std::{collections::HashSet, sync::Arc}; +pub(crate) mod additional_trust_bundle; pub(crate) mod cluster_domain_rename; mod fnv; +mod go_base32; pub(crate) mod hostname_rename; pub(crate) mod ip_rename; pub(crate) mod pull_secret_rename; @@ -25,8 +27,6 @@ pub(crate) mod pull_secret_rename; pub(crate) async fn ocp_postprocess( in_memory_etcd_client: &Arc, cluster_customizations: &ClusterCustomizations, - static_dirs: &Vec, - static_files: &Vec, ) -> Result<()> { fix_olm_secret_hash_annotation(in_memory_etcd_client) .await @@ -40,11 +40,11 @@ pub(crate) async fn ocp_postprocess( .await .context("deleting node-kubeconfigs")?; - sync_webhook_authenticators(in_memory_etcd_client, static_dirs) + sync_webhook_authenticators(in_memory_etcd_client, &cluster_customizations.dirs) .await .context("syncing webhook authenticators")?; - run_cluster_customizations(cluster_customizations, in_memory_etcd_client, static_dirs, static_files).await?; + run_cluster_customizations(cluster_customizations, in_memory_etcd_client).await?; fix_deployment_dep_annotations( in_memory_etcd_client, @@ -66,25 +66,24 @@ pub(crate) async fn ocp_postprocess( async fn run_cluster_customizations( cluster_customizations: &ClusterCustomizations, in_memory_etcd_client: &Arc, - static_dirs: &Vec, - static_files: &Vec, ) -> Result<(), anyhow::Error> { + let dirs = &cluster_customizations.dirs; + let files = &cluster_customizations.files; + if let Some(cluster_names_rename) = &cluster_customizations.cluster_rename { - cluster_rename(in_memory_etcd_client, cluster_names_rename, static_dirs, static_files) + cluster_rename(in_memory_etcd_client, cluster_names_rename, dirs, files) .await .context("renaming cluster")?; } if let Some(hostname) = &cluster_customizations.hostname { - hostname_rename(in_memory_etcd_client, hostname, static_dirs, static_files) + hostname_rename(in_memory_etcd_client, hostname, dirs, files) .await .context("renaming hostname")?; } if let Some(ip) = &cluster_customizations.ip { - ip_rename(in_memory_etcd_client, ip, static_dirs, static_files) - .await - .context("renaming IP")?; + ip_rename(in_memory_etcd_client, ip, dirs, files).await.context("renaming IP")?; } if let Some(kubeadmin_password_hash) = &cluster_customizations.kubeadmin_password_hash { @@ -96,11 +95,17 @@ async fn run_cluster_customizations( if let Some(pull_secret) = &cluster_customizations.pull_secret { log::info!("setting new pull_secret"); - pull_secret_rename(in_memory_etcd_client, pull_secret, static_dirs, static_files) + pull_secret_rename(in_memory_etcd_client, pull_secret, dirs, files) .await .context("renaming pull_secret")?; }; + if let Some(additional_trust_bundle) = &cluster_customizations.additional_trust_bundle { + additional_trust_bundle_rename(in_memory_etcd_client, additional_trust_bundle, dirs, files) + .await + .context("renaming additional_trust_bundle")?; + } + Ok(()) } @@ -485,3 +490,18 @@ pub(crate) async fn pull_secret_rename( Ok(()) } + +pub(crate) async fn additional_trust_bundle_rename( + in_memory_etcd_client: &Arc, + additional_trust_bundle: &str, + static_dirs: &[ConfigPath], + static_files: &[ConfigPath], +) -> Result<()> { + let etcd_client = in_memory_etcd_client; + + additional_trust_bundle::rename_all(etcd_client, additional_trust_bundle, static_dirs, static_files) + .await + .context("renaming all")?; + + Ok(()) +} diff --git a/src/ocp_postprocess/additional_trust_bundle.rs b/src/ocp_postprocess/additional_trust_bundle.rs new file mode 100644 index 00000000..6e08f170 --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle.rs @@ -0,0 +1,95 @@ +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use anyhow::{Context, Result}; +use std::{path::Path, sync::Arc}; + +mod etcd_rename; +mod filesystem_rename; +mod utils; + +pub(crate) async fn rename_all( + etcd_client: &Arc, + additional_trust_bundle: &str, + static_dirs: &[ConfigPath], + static_files: &[ConfigPath], +) -> Result<()> { + let new_merged_bundle = fix_etcd_resources(etcd_client, additional_trust_bundle) + .await + .context("renaming etcd resources")?; + + fix_filesystem_resources(&new_merged_bundle, additional_trust_bundle, static_dirs, static_files) + .await + .context("renaming filesystem resources")?; + + Ok(()) +} + +async fn fix_filesystem_resources( + additional_trust_bundle: &str, + new_merged_bundle: &str, + static_dirs: &[ConfigPath], + static_files: &[ConfigPath], +) -> Result<()> { + for dir in static_dirs { + fix_dir_resources(additional_trust_bundle, new_merged_bundle, dir).await?; + } + for file in static_files { + fix_file_resources(additional_trust_bundle, new_merged_bundle, file).await?; + } + + Ok(()) +} + +async fn fix_dir_resources(additional_trust_bundle: &str, new_merged_bundle: &str, dir: &Path) -> Result<()> { + // NOTE: This only fixes the trust anchors, the user should run "update-ca-trust" to fully + // update the system trust store after this change (this is also what MCO does). + filesystem_rename::fix_filesystem_ca_trust_anchors(additional_trust_bundle, dir) + .await + .context("fixing ca trust anchors")?; + + filesystem_rename::fix_filesystem_currentconfig(additional_trust_bundle, dir) + .await + .context("renaming currentconfig")?; + + filesystem_rename::fix_static_configmap_trusted_ca_bundle(new_merged_bundle, dir) + .await + .context("fixing static configmap trusted ca bundle")?; + + Ok(()) +} + +async fn fix_file_resources(_additional_trust_bundle: &str, _new_merged_bundle: &str, _file: &Path) -> Result<()> { + Ok(()) +} + +async fn fix_etcd_resources(etcd_client: &Arc, additional_trust_bundle: &str) -> Result { + // kubernetes.io/configmaps/openshift-config/custom-ca + let original_additional_trust_bundle = etcd_rename::fix_original_additional_trust_bundle(etcd_client, additional_trust_bundle) + .await + .context("fixing labeled configmaps")?; + + let system_certs = utils::derive_system_certs_from_merged_bundle( + original_additional_trust_bundle, + utils::get_merged_bundle(etcd_client).await.context("getting merged bundle")?, + ) + .context("getting unmerged bundle")?; + + let new_merged_bundle = utils::merge_bundles(additional_trust_bundle, &system_certs); + + etcd_rename::fix_labeled_configmaps(etcd_client, &new_merged_bundle) + .await + .context("fixing labeled configmaps")?; + + etcd_rename::fix_monitoring_configmaps(etcd_client, &new_merged_bundle) + .await + .context("fixing labeled configmaps")?; + + etcd_rename::fix_machineconfigs(etcd_client, additional_trust_bundle) + .await + .context("fixing machineconfigs")?; + + etcd_rename::fix_kcm_openshift_user_ca(etcd_client, additional_trust_bundle) + .await + .context("fixing kcm openshift user ca")?; + + Ok(new_merged_bundle) +} diff --git a/src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs b/src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs new file mode 100644 index 00000000..4330504b --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs @@ -0,0 +1,270 @@ +use super::utils::fix_machineconfig; +use crate::{ + cluster_crypto::locations::K8sResourceLocation, + k8s_etcd::{get_etcd_json, put_etcd_yaml, InMemoryK8sEtcd}, + ocp_postprocess::fnv::fnv1_64, + ocp_postprocess::go_base32::base32_encode as go_base32_encode, +}; +use anyhow::{ensure, Context, Result}; +use futures_util::future::join_all; +use regex::Regex; +use serde_json::Value; +use std::sync::Arc; + +pub(crate) async fn fix_machineconfigs(etcd_client: &Arc, additional_trust_bundle: &str) -> Result<()> { + join_all( + etcd_client + .list_keys("machineconfiguration.openshift.io/machineconfigs") + .await? + .into_iter() + .map(|key| async move { + let etcd_result = etcd_client + .get(key.clone()) + .await + .with_context(|| format!("getting key {:?}", key))? + .context("key disappeared")?; + let value: Value = serde_yaml::from_slice(etcd_result.value.as_slice()) + .with_context(|| format!("deserializing value of key {:?}", key,))?; + let k8s_resource_location = K8sResourceLocation::try_from(&value)?; + + let mut machineconfig = get_etcd_json(etcd_client, &k8s_resource_location) + .await? + .context("no machineconfig")?; + + fix_machineconfig(&mut machineconfig, additional_trust_bundle).context("fixing machineconfig")?; + + put_etcd_yaml(etcd_client, &k8s_resource_location, machineconfig).await?; + + Ok(()) + }), + ) + .await + .into_iter() + .collect::>>()?; + + Ok(()) +} + +// There's an OCP operator that injects the trusted CA bundle into configmaps which have this +// label. We simply emulate that behavior here, should be a bit more robust than hardcoding a list +// of configmaps +pub(crate) async fn fix_labeled_configmaps(etcd_client: &InMemoryK8sEtcd, full_merged_bundle: &str) -> Result<()> { + join_all(etcd_client.list_keys("configmaps/").await?.into_iter().map(|key| async move { + let etcd_result = etcd_client + .get(key.clone()) + .await + .with_context(|| format!("getting key {:?}", key))? + .context("key disappeared")?; + let value: Value = + serde_yaml::from_slice(etcd_result.value.as_slice()).with_context(|| format!("deserializing value of key {:?}", key,))?; + + let slash = "~1"; + if value + .pointer(&format!("/metadata/labels/config.openshift.io{slash}inject-trusted-cabundle")) + .is_none() + { + let unlabeled_exceptions = [ + // All other certs are injected from this configmap, so we need to fix it as well, + // even though it's not labeled + K8sResourceLocation::new(Some("openshift-config-managed"), "ConfigMap", "trusted-ca-bundle", "v1"), + // ccm is quirky and builds its own configmap with a merged bundle. Usually it + // contains also certs taken from the cloud config, so it could look different than + // what network operator injects, but since we're doing SNO-none, the result is + // identical to what the network operator injects, so we can fix it as well + K8sResourceLocation::new(Some("openshift-cloud-controller-manager"), "ConfigMap", "ccm-trusted-ca", "v1"), + ]; + + if unlabeled_exceptions.iter().all(|location| location.as_etcd_key() != key) { + // This is not a configmap we want to inject into and neither is it the source of + // the injection, so it doesn't need to be fixed + return Ok(()); + } + } + + let k8s_resource_location = K8sResourceLocation::try_from(&value)?; + + let mut configmap = get_etcd_json(etcd_client, &k8s_resource_location) + .await? + .context("no machineconfig")?; + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(full_merged_bundle.to_string()), + ); + + put_etcd_yaml(etcd_client, &k8s_resource_location, configmap).await?; + + Ok(()) + })) + .await + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_original_additional_trust_bundle(etcd_client: &InMemoryK8sEtcd, additional_trust_bundle: &str) -> Result { + let proxy_config_k8s_resource_location = K8sResourceLocation::new(None, "Proxy", "cluster", "config.openshift.io"); + + let config = get_etcd_json(etcd_client, &proxy_config_k8s_resource_location) + .await? + .context("could not find proxy cluster config")?; + + let trusted_ca_configmap_name = config + .pointer("/spec/trustedCA/name") + .context("no trustedCA in proxy cluster config")? + .as_str() + .context("trustedCA not a string")?; + + let ca_configmap_k8s_resource_location = + K8sResourceLocation::new(Some("openshift-config"), "ConfigMap", trusted_ca_configmap_name, "v1"); + + let mut configmap = get_etcd_json(etcd_client, &ca_configmap_k8s_resource_location) + .await? + .context("could not find trustedCA configmap")?; + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + let original_additional_trust_bundle = data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(additional_trust_bundle.to_string()), + ); + + put_etcd_yaml(etcd_client, &ca_configmap_k8s_resource_location, configmap).await?; + + Ok(original_additional_trust_bundle + .context("no ca-bundle.crt in trustedCA configmap")? + .as_str() + .context("ca-bundle.crt not a string")? + .to_string()) +} + +pub(crate) async fn fix_monitoring_configmaps(etcd_client: &InMemoryK8sEtcd, new_merged_bundle: &str) -> Result<()> { + join_all( + etcd_client + .list_keys("configmaps/openshift-monitoring/") + .await? + .into_iter() + .map(|key| async move { + let etcd_result = etcd_client + .get(key.clone()) + .await + .with_context(|| format!("getting key {:?}", key))? + .context("key disappeared")?; + let value: Value = serde_json::from_slice(etcd_result.value.as_slice()) + .with_context(|| format!("deserializing value of key {:?}", key,))?; + + let k8s_resource_location = K8sResourceLocation::try_from(&value)?; + + let regex = &Regex::new(r"(?P.*)-trusted-ca-bundle-(?P[0-9a-z]+)").context("compiling regex")?; + let matches = regex.captures(&k8s_resource_location.name); + + let matches = match matches { + Some(matches) => matches, + None => return Ok(()), + }; + + let component = matches.name("component").context("no component")?.as_str(); + + let mut configmap = get_etcd_json(etcd_client, &k8s_resource_location) + .await? + .context("no machineconfig")?; + + let current_data = configmap + .pointer("/data/ca-bundle.crt") + .context("no ca-bundle.crt in configmap")? + .as_str() + .context("ca-bundle.crt not a string")? + .as_bytes(); + + let recert_calculated_original_hash = go_base32_encode(fnv1_64(current_data)); + + let operator_calculated_hash = configmap + .pointer("/metadata/labels/monitoring.openshift.io~1hash") + .context("no monitoring.openshift.io/hash in configmap")? + .as_str() + .context("monitoring.openshift.io/hash not a string")?; + + // Sanity check to make sure our hash function is compatible with the one used by + // the monitoring operator + ensure!( + recert_calculated_original_hash == operator_calculated_hash, + format!("hash mismatch: {} != {}", recert_calculated_original_hash, operator_calculated_hash) + ); + + let new_hash = go_base32_encode(fnv1_64(new_merged_bundle.as_bytes())); + + configmap + .pointer_mut("/metadata/labels") + .context("no /metadata/labels in configmap")? + .as_object_mut() + .context("/metadata/labels not an object")? + .insert( + "monitoring.openshift.io/hash".to_string(), + serde_json::Value::String(new_hash.clone()), + ); + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(new_merged_bundle.to_string()), + ); + + let new_resource_location = K8sResourceLocation::new( + k8s_resource_location.namespace.as_deref(), + &k8s_resource_location.kind, + &format!("{component}-trusted-ca-bundle-{new_hash}"), + &k8s_resource_location.apiversion, + ); + + put_etcd_yaml(etcd_client, &new_resource_location, configmap) + .await + .context("putting new configmap")?; + + etcd_client.delete(&key).await.context("deleting old configmap")?; + + Ok(()) + }), + ) + .await + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_kcm_openshift_user_ca(etcd_client: &InMemoryK8sEtcd, additional_trust_bundle: &str) -> Result<()> { + let k8s_resource_location = K8sResourceLocation::new(Some("openshift-controller-manager"), "ConfigMap", "openshift-user-ca", "v1"); + + let mut configmap = get_etcd_json(etcd_client, &k8s_resource_location).await?.context("no configmap")?; + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(additional_trust_bundle.to_string()), + ); + + put_etcd_yaml(etcd_client, &k8s_resource_location, configmap).await?; + + Ok(()) +} diff --git a/src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs b/src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs new file mode 100644 index 00000000..debc291b --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs @@ -0,0 +1,95 @@ +use crate::file_utils::{self, commit_file, read_file_to_string}; +use anyhow::{self, Context, Result}; +use futures_util::future::join_all; +use serde_json::Value; +use std::path::Path; + +use super::utils::fix_machineconfig; + +pub(crate) async fn fix_filesystem_ca_trust_anchors(additional_trust_bundle: &str, dir: &Path) -> Result<()> { + join_all( + file_utils::globvec(dir, "**/anchors/openshift-config-user-ca-bundle.crt")? + .into_iter() + .map(|file_path| { + let crt_file_path = file_path.clone(); + let additional_trust_bundle = additional_trust_bundle.to_string(); + tokio::spawn(async move { + async move { + commit_file(file_path, additional_trust_bundle.clone()) + .await + .context("writing to disk")?; + + anyhow::Ok(()) + } + .await + .context("fixing system CA bundle") + }) + }), + ) + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_filesystem_currentconfig(additional_trust_bundle: &str, dir: &Path) -> Result<()> { + join_all(file_utils::globvec(dir, "**/currentconfig")?.into_iter().map(|file_path| { + let kcm_config_path = file_path.clone(); + let additional_trust_bundle = additional_trust_bundle.to_string(); + tokio::spawn(async move { + async move { + let contents = read_file_to_string(&file_path) + .await + .context("reading currentconfig")?; + let mut config: Value = serde_json::from_str(&contents).context("parsing currentconfig")?; + + fix_machineconfig(&mut config, &additional_trust_bundle)?; + + commit_file(file_path, serde_json::to_string(&config).context("serializing currentconfig")?) + .await + .context("writing currentconfig to disk")?; + + anyhow::Ok(()) + } + .await + .context(format!("fixing currentconfig {:?}", kcm_config_path)) + }) + })) + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_static_configmap_trusted_ca_bundle(new_merged_bundle: &str, dir: &Path) -> Result<()> { + join_all( + file_utils::globvec(dir, "**/configmaps/trusted-ca-bundle/ca-bundle.crt")? + .into_iter() + .map(|file_path| { + let ca_bundle_path = file_path.clone(); + let new_merged_bundle = new_merged_bundle.to_string(); + tokio::spawn(async move { + async move { + commit_file(file_path, new_merged_bundle.clone()).await.context("writing to disk")?; + + anyhow::Ok(()) + } + .await + .context(format!("fixing static configmap trusted ca bundle {:?}", ca_bundle_path)) + }) + }), + ) + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::>>()?; + + Ok(()) +} diff --git a/src/ocp_postprocess/additional_trust_bundle/utils.rs b/src/ocp_postprocess/additional_trust_bundle/utils.rs new file mode 100644 index 00000000..30d25283 --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle/utils.rs @@ -0,0 +1,88 @@ +use crate::{ + cluster_crypto::locations::K8sResourceLocation, + file_utils, + k8s_etcd::{get_etcd_json, InMemoryK8sEtcd}, +}; +use anyhow::{Context, Result}; +use serde_json::Value; + +pub(crate) fn fix_machineconfig(machineconfig: &mut Value, additional_trust_bundle: &str) -> Result<()> { + let pointer_mut = machineconfig.pointer_mut("/spec/config/storage/files"); + if pointer_mut.is_none() { + // Not all machineconfigs have files to look at and that's ok + return Ok(()); + }; + + let find_map = pointer_mut + .context("no /spec/config/storage/files")? + .as_array_mut() + .context("files not an array")? + .iter_mut() + .find_map(|file| { + (file.pointer("/path")? == "/etc/pki/ca-trust/source/anchors/openshift-config-user-ca-bundle.crt").then_some(file) + }); + + if find_map.is_none() { + // Not all machineconfigs have the file we're looking for and that's ok + return Ok(()); + }; + + let file_contents = find_map + .context("no /etc/kubernetes/apiserver-url.env file in machineconfig")? + .pointer_mut("/contents") + .context("no .contents")? + .as_object_mut() + .context("annotations not an object")?; + + file_contents.insert( + "source".to_string(), + serde_json::Value::String(file_utils::dataurl_encode(additional_trust_bundle)), + ); + + Ok(()) +} + +pub(crate) async fn get_merged_bundle(etcd_client: &InMemoryK8sEtcd) -> Result { + let k8s_resource_location = K8sResourceLocation::new(Some("openshift-config-managed"), "ConfigMap", "trusted-ca-bundle", "v1"); + + let config = get_etcd_json(etcd_client, &k8s_resource_location) + .await + .context("failed to get trusted-ca-bundle configmap")? + .context("could not find trusted-ca-bundle configmap")?; + + let data = config + .pointer("/data/ca-bundle.crt") + .context("no ca-bundle.crt in trusted-ca-bundle configmap")? + .as_str() + .context("ca-bundle.crt not a string")?; + + Ok(data.to_string()) +} + +/// There's no place where we can get just the system certificates, that don't already contain the +/// seed's additional trust bundle, so we have to derive it ourselves by taking the entire merged +/// bundle and removing from it just the certs that also appear in the seed's additional trust +/// bundle. What's left after removal should be just the seed's system certs +pub(crate) fn derive_system_certs_from_merged_bundle(original_additional_trust_bundle: String, merged_bundle: String) -> Result { + let last_original_cert = pem::parse_many(original_additional_trust_bundle.as_bytes()) + .context("failed to parse original additional trust bundle")? + .into_iter() + .last() + .context("no certs in original additional trust bundle")?; + + let last_original_cert_encoded = + pem::encode_config(&last_original_cert, pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF)).to_string(); + let position_of_last_original_cert = merged_bundle + .find(&last_original_cert_encoded) + .context("last original cert not found in merged bundle")?; + + let system_certs = merged_bundle + .get(position_of_last_original_cert + last_original_cert_encoded.len()..) + .context("failed to get system certs")?; + + Ok(system_certs.to_string()) +} + +pub(crate) fn merge_bundles(additional_trust_bundle: &str, system_certs: &str) -> String { + format!("{}{}", additional_trust_bundle, system_certs) +} diff --git a/src/ocp_postprocess/cluster_domain_rename.rs b/src/ocp_postprocess/cluster_domain_rename.rs index 12b1d771..b85beec6 100644 --- a/src/ocp_postprocess/cluster_domain_rename.rs +++ b/src/ocp_postprocess/cluster_domain_rename.rs @@ -1,5 +1,5 @@ use self::params::ClusterNamesRename; -use crate::{cluster_crypto::locations::K8sResourceLocation, config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{cluster_crypto::locations::K8sResourceLocation, config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/ocp_postprocess/cluster_domain_rename/params.rs b/src/ocp_postprocess/cluster_domain_rename/params.rs index caff0ea5..781423d9 100644 --- a/src/ocp_postprocess/cluster_domain_rename/params.rs +++ b/src/ocp_postprocess/cluster_domain_rename/params.rs @@ -8,7 +8,7 @@ pub(crate) struct ClusterNamesRename { } impl ClusterNamesRename { - pub(crate) fn cli_parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { let parts = value.split(':').collect::>(); ensure!( diff --git a/src/ocp_postprocess/fnv.rs b/src/ocp_postprocess/fnv.rs index fe44616d..6f96a9f6 100644 --- a/src/ocp_postprocess/fnv.rs +++ b/src/ocp_postprocess/fnv.rs @@ -6,3 +6,12 @@ pub(crate) fn fnv1_32(data: &[u8]) -> u32 { } hash } + +pub(crate) fn fnv1_64(data: &[u8]) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in data { + hash = hash.wrapping_mul(0x100000001b3); + hash ^= u64::from(*byte); + } + hash +} diff --git a/src/ocp_postprocess/go_base32.rs b/src/ocp_postprocess/go_base32.rs new file mode 100644 index 00000000..dfa96628 --- /dev/null +++ b/src/ocp_postprocess/go_base32.rs @@ -0,0 +1,65 @@ +const BASE32_DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz"; +const BASE32: u64 = 32; +const BASE32_MASK: u64 = BASE32 - 1; +const BASE32_SHIFT: u64 = 5; +const MAX_BASE32_U64_DIGITS: usize = 64 + 1; + +pub(crate) fn base32_encode(mut num: u64) -> String { + let mut output_array = [0u8; MAX_BASE32_U64_DIGITS]; + let mut output_index = output_array.len(); + + while num >= BASE32 { + output_index -= 1; + output_array[output_index] = BASE32_DIGITS[(num & BASE32_MASK) as usize]; + + num >>= BASE32_SHIFT; + } + + output_index -= 1; + output_array[output_index] = BASE32_DIGITS[num as usize]; + + String::from_utf8(output_array[output_index..].to_vec()).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + // package main + // import ( + // "math/rand" + // "fmt" + // "strconv" + // ) + // func main() { + // for i := 0; i < 10; i++ { + // fmt.Printf("assert_eq!(base32_encode(%d), %q);\n", i, strconv.FormatUint(uint64(i), 32)) + // } + // for i := 0; i < 10; i++ { + // x := rand.Uint64() + // fmt.Printf("assert_eq!(base32_encode(%d), %q);\n", x, strconv.FormatUint(x, 32)) + // } + // } + fn test_base32_encode() { + assert_eq!(base32_encode(0), "0"); + assert_eq!(base32_encode(1), "1"); + assert_eq!(base32_encode(2), "2"); + assert_eq!(base32_encode(3), "3"); + assert_eq!(base32_encode(4), "4"); + assert_eq!(base32_encode(5), "5"); + assert_eq!(base32_encode(6), "6"); + assert_eq!(base32_encode(7), "7"); + assert_eq!(base32_encode(8), "8"); + assert_eq!(base32_encode(9), "9"); + assert_eq!(base32_encode(9571486601897812948), "89l60b8v8f8uk"); + assert_eq!(base32_encode(14972217520619435323), "cvi00g906dd9r"); + assert_eq!(base32_encode(828543677970655149), "mvsk2nb5c9td"); + assert_eq!(base32_encode(10165118770545495894), "8q4e1l8l0a3qm"); + assert_eq!(base32_encode(13003616107026089108), "b8tgvug3dd14k"); + assert_eq!(base32_encode(17310030582050129450), "f0ecl38b3u1ha"); + assert_eq!(base32_encode(5246586088816823604), "4hjsu805mtj9k"); + assert_eq!(base32_encode(9506301410689701343), "87r9aogrv8lev"); + assert_eq!(base32_encode(17418413831566181087), "f3el32vak1gmv"); + assert_eq!(base32_encode(10879890617200402116), "9dv92rt4lbnm4"); + } +} diff --git a/src/ocp_postprocess/hostname_rename.rs b/src/ocp_postprocess/hostname_rename.rs index d8a9c178..15712204 100644 --- a/src/ocp_postprocess/hostname_rename.rs +++ b/src/ocp_postprocess/hostname_rename.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/ocp_postprocess/ip_rename.rs b/src/ocp_postprocess/ip_rename.rs index ffb90be5..ad7ae404 100644 --- a/src/ocp_postprocess/ip_rename.rs +++ b/src/ocp_postprocess/ip_rename.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/ocp_postprocess/pull_secret_rename.rs b/src/ocp_postprocess/pull_secret_rename.rs index 8cb648fc..d08cdd06 100644 --- a/src/ocp_postprocess/pull_secret_rename.rs +++ b/src/ocp_postprocess/pull_secret_rename.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/recert.rs b/src/recert.rs index 3289a803..51cf87c6 100644 --- a/src/recert.rs +++ b/src/recert.rs @@ -1,6 +1,6 @@ use crate::{ cluster_crypto::{crypto_utils::ensure_openssl_version, scanning, ClusterCryptoObjects}, - config::{ClusterCustomizations, ConfigPath, CryptoCustomizations, RecertConfig}, + config::{ClusterCustomizations, CryptoCustomizations, RecertConfig}, k8s_etcd::InMemoryK8sEtcd, ocp_postprocess::ocp_postprocess, rsa_key_pool, server_ssh_keys, @@ -21,8 +21,6 @@ pub(crate) async fn run(recert_config: &RecertConfig, cluster_crypto: &mut Clust let recertify_timing = recertify( cluster_crypto, Arc::clone(&in_memory_etcd_client), - recert_config.static_dirs.clone(), - recert_config.static_files.clone(), &recert_config.crypto_customizations, ) .await @@ -32,8 +30,6 @@ pub(crate) async fn run(recert_config: &RecertConfig, cluster_crypto: &mut Clust Arc::clone(&in_memory_etcd_client), cluster_crypto, &recert_config.cluster_customizations, - &recert_config.static_dirs, - &recert_config.static_files, recert_config.regenerate_server_ssh_keys.as_deref(), recert_config.dry_run, ) @@ -58,8 +54,6 @@ async fn get_etcd_endpoint(recert_config: &RecertConfig) -> Result, - static_dirs: Vec, - static_files: Vec, crypto_customizations: &CryptoCustomizations, ) -> Result { let external_certs = if in_memory_etcd_client.etcd_client.is_some() { @@ -74,8 +68,8 @@ async fn recertify( // a long time and are independent let all_discovered_crypto_objects = tokio::spawn(scanning::crypto_scan( in_memory_etcd_client, - static_dirs, - static_files, + crypto_customizations.dirs.clone(), + crypto_customizations.files.clone(), external_certs.clone(), )); let rsa_keys = tokio::spawn(fill_keys()); @@ -108,8 +102,6 @@ async fn finalize( in_memory_etcd_client: Arc, cluster_crypto: &mut ClusterCryptoObjects, cluster_customizations: &ClusterCustomizations, - static_dirs: &Vec, - static_files: &Vec, regenerate_server_ssh_keys: Option<&Path>, dry_run: bool, ) -> Result { @@ -122,7 +114,7 @@ async fn finalize( let start = std::time::Instant::now(); if in_memory_etcd_client.etcd_client.is_some() { - ocp_postprocess(&in_memory_etcd_client, cluster_customizations, static_dirs, static_files) + ocp_postprocess(&in_memory_etcd_client, cluster_customizations) .await .context("performing ocp specific post-processing")?; } diff --git a/src/use_cert.rs b/src/use_cert.rs index fc2b40f6..cd50ee4b 100644 --- a/src/use_cert.rs +++ b/src/use_cert.rs @@ -18,7 +18,7 @@ impl std::fmt::Display for UseCert { } impl UseCert { - pub(crate) fn cli_parse(cert_path_or_pem: &str) -> Result { + pub(crate) fn parse(cert_path_or_pem: &str) -> Result { let pem = pem::parse_many(if cert_path_or_pem.contains('\n') { cert_path_or_pem.as_bytes().to_vec() } else { diff --git a/src/use_key.rs b/src/use_key.rs index 8bdce6f1..c35104fd 100644 --- a/src/use_key.rs +++ b/src/use_key.rs @@ -40,7 +40,7 @@ impl std::fmt::Display for UseKey { } impl UseKey { - pub(crate) fn cli_parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { // TODO: ' ' is legacy, remove eventually let parts = if value.contains(':') { value.split(':') } else { value.split(' ') }.collect::>();