From 0e2f362f80a78bbe9cba8806e47da7dea200d0ac Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Fri, 27 Sep 2024 16:47:55 -0300 Subject: [PATCH 01/16] Nonkube support for user provided certificates --- internal/nonkube/bundle/install.sh.template | 100 +++++++++++++++++++- pkg/nonkube/api/environment.go | 21 ++-- pkg/nonkube/api/token.go | 29 +++++- pkg/nonkube/common/fs_config_renderer.go | 57 ++++++++++- 4 files changed, 187 insertions(+), 20 deletions(-) diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index cb786b85a..44cc30c07 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -239,7 +239,7 @@ create_containers() { } create_site() { - if [ -d "${NAMESPACES_PATH}/${NAMESPACE}" ]; then + if [ -d "${NAMESPACES_PATH}/${NAMESPACE}/runtime" ]; then echo "Failed: namespace \"${NAMESPACE}\" is already defined" echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}" exit 1 @@ -253,10 +253,10 @@ create_site() { echo "Version : ${VERSION}" # Create base directory tree - mkdir -p "${NAMESPACES_PATH}" + mkdir -p "${NAMESPACES_PATH}/${NAMESPACE}" # Installing site definition files - cp -rf "./${SOURCE_NAMESPACE}" "${NAMESPACES_PATH}/${NAMESPACE}" + cp -rf "./${SOURCE_NAMESPACE}"/* "${NAMESPACES_PATH}/${NAMESPACE}/" # Creating platform.yaml echo "platform: ${SKUPPER_PLATFORM}" > "${PLATFORM_FILE}" @@ -282,7 +282,7 @@ create_site() { sanity_check() { required_fields="SITE_NAME SOURCE_NAMESPACE NAMESPACE SKUPPER_OUTPUT_PATH SERVICE_DIR NAMESPACES_PATH SKUPPER_PLATFORM" - required_commands="python sed find grep xargs tar getent id ${PLATFORM_COMMAND}" + required_commands="python sed find grep wc xargs tar getent id ${PLATFORM_COMMAND}" for field_name in ${required_fields}; do eval [ -n "\${${field_name}}" ] || exit_error "Internal error: required field ${field_name} not defined" @@ -294,12 +294,104 @@ sanity_check() { done } +copy_user_certificates() { + export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates" + [ ! -d "${USER_CERTIFICATES_PATH}" ] && return + cas="" + clients="" + servers="" + if [ -d "${USER_CERTIFICATES_PATH}"/ca ]; then + cas=$(cd "${USER_CERTIFICATES_PATH}"/ca; ls -1) + fi + if [ -d "${USER_CERTIFICATES_PATH}"/client ]; then + clients=$(cd "${USER_CERTIFICATES_PATH}"/client; ls -1) + fi + if [ -d "${USER_CERTIFICATES_PATH}"/server ]; then + servers=$(cd "${USER_CERTIFICATES_PATH}"/server; ls -1) + fi + if [ -z "${cas}" ] && [ -z "${clients}" ] && [ -z "${servers}" ]; then + return + fi + echo "User provided certificates found:" + if [ -n "${cas}" ]; then + echo "- CA certificates:" + for ca in ${cas}; do + echo " - ${ca}" + done + fi + if [ -n "${clients}" ]; then + echo "- Client certificates:" + for client in ${clients}; do + echo " - ${client}" + done + fi + if [ -n "${servers}" ]; then + echo "- Server certificates:" + for server in ${servers}; do + echo " - ${server}" + done + fi + cp -r "${USER_CERTIFICATES_PATH}"/* "./${SOURCE_NAMESPACE}/certificates" + update_static_links +} + +update_static_links() { + DIR="./${SOURCE_NAMESPACE}" + export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates" + router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f) + for ra in ${router_access_files}; do + ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g') + tls_credential=$(grep tlsCredentials: "${ra}" | awk '{print $NF}' | sed 's/"//g') + bind_host=$(grep bindHost: "${ra}" | awk '{print $NF}' | sed 's/"//g') + input_cert_path="${USER_CERTIFICATES_PATH}/client/client-${tls_credential:-${ra_name}}" + [ ! -d "${input_cert_path}" ] && continue + found=true + for file in tls.key tls.crt ca.crt; do + if [ ! -f "${input_cert_path}/${file}" ]; then + found=false + break + fi + done + ! ${found} && continue + # client certs found, reading client certificates + ca_crt=$(base64 -w0 "${input_cert_path}/ca.crt") + tls_crt=$(base64 -w0 "${input_cert_path}/tls.crt") + tls_key=$(base64 -w0 "${input_cert_path}/tls.key") + # find all static links generated for the given RouterAccess + static_link_files=$(find "${DIR:?}/runtime/link" -type f -name "link-${ra_name}-*") + for link in ${static_link_files}; do + echo "- Updating credentials on static link: ${link}" + sed -i "s/tls.key: .*/tls.key: ${tls_key}/g" "${link}" + sed -i "s/tls.crt: .*/tls.crt: ${tls_crt}/g" "${link}" + sed -i "s/ca.crt: .*/ca.crt: ${ca_crt}/g" "${link}" + done + # if openssl is present, discover SANs and create missing static links + if ! command -v openssl > /dev/null 2>&1; then + continue + fi + # discovering SANS + server_cert="${DIR}/certificates/server/${tls_credential:-${ra_name}}/tls.crt" + [ ! -f "${server_cert}" ] && continue + subjects="$(openssl x509 -noout -ext subjectAltName -in "${server_cert}" |grep -v 'Subject Alternative Name:' | sed 's/ //g;s/,/\n/g' | awk -F: '{print $2}')" + [ -z "${subjects}" ] && continue + for subject in ${subjects}; do + link_file="${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" + [ -f "${link_file}" ] && continue + echo "creating static link for new server subject found: ${subject}" + cp "${DIR}/runtime/link/link-${ra_name}-${bind_host:-127.0.0.1}.yaml" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" + sed -i "s/host: .*/host: ${subject}/g" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" + done + done +} + main() { # validate provided options parse_opts "$@" sanity_check + copy_user_certificates + if ${REMOVE}; then remove_site remove_service diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go index ac41b26b1..2902d8eed 100644 --- a/pkg/nonkube/api/environment.go +++ b/pkg/nonkube/api/environment.go @@ -12,15 +12,18 @@ type InternalPath string type InternalPathProvider func(namespace string, internalPath InternalPath) string const ( - ConfigRouterPath InternalPath = "config/router" - CertificatesCaPath InternalPath = "certificates/ca" - CertificatesClientPath InternalPath = "certificates/client" - CertificatesServerPath InternalPath = "certificates/server" - CertificatesLinkPath InternalPath = "certificates/link" - LoadedSiteStatePath InternalPath = "sources" - RuntimeSiteStatePath InternalPath = "runtime/state" - RuntimeTokenPath InternalPath = "runtime/link" - RuntimeScriptsPath InternalPath = "runtime/scripts" + ConfigRouterPath InternalPath = "config/router" + CertificatesCaPath InternalPath = "certificates/ca" + CertificatesClientPath InternalPath = "certificates/client" + CertificatesServerPath InternalPath = "certificates/server" + CertificatesLinkPath InternalPath = "certificates/link" + InputCertificatesCaPath InternalPath = "input/certificates/ca" + InputCertificatesClientPath InternalPath = "input/certificates/client" + InputCertificatesServerPath InternalPath = "input/certificates/server" + LoadedSiteStatePath InternalPath = "sources" + RuntimeSiteStatePath InternalPath = "runtime/state" + RuntimeTokenPath InternalPath = "runtime/link" + RuntimeScriptsPath InternalPath = "runtime/scripts" ) type IdGetter func() int diff --git a/pkg/nonkube/api/token.go b/pkg/nonkube/api/token.go index 8dcf575a4..13c0532d0 100644 --- a/pkg/nonkube/api/token.go +++ b/pkg/nonkube/api/token.go @@ -3,7 +3,10 @@ package api import ( "bufio" "bytes" + "crypto/x509" + "encoding/pem" "fmt" + "slices" "strconv" "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1" @@ -41,7 +44,7 @@ func (t *Token) Marshal() ([]byte, error) { return buffer.Bytes(), nil } -func CreateTokens(routerAccess v1alpha1.RouterAccess, secret v1.Secret) []*Token { +func CreateTokens(routerAccess v1alpha1.RouterAccess, serverSecret v1.Secret, clientSecret v1.Secret) []*Token { var tokens []*Token interRouter := 0 edge := 0 @@ -60,7 +63,7 @@ func CreateTokens(routerAccess v1alpha1.RouterAccess, secret v1.Secret) []*Token name := routerAccess.Name linkName := fmt.Sprintf("link-%s", name) // adjusting name to match the standard used by pkg/site/link.go - secret.Name = fmt.Sprintf("link-%s", name) + clientSecret.Name = fmt.Sprintf("link-%s", name) token := &Token{ Links: []*v1alpha1.Link{ { @@ -72,12 +75,12 @@ func CreateTokens(routerAccess v1alpha1.RouterAccess, secret v1.Secret) []*Token Name: linkName, }, Spec: v1alpha1.LinkSpec{ - TlsCredentials: secret.Name, + TlsCredentials: clientSecret.Name, Cost: 1, }, }, }, - Secret: &secret, + Secret: &clientSecret, } var endpoints []v1alpha1.Endpoint if interRouter > 0 { @@ -105,6 +108,24 @@ func CreateTokens(routerAccess v1alpha1.RouterAccess, secret v1.Secret) []*Token if len(routerAccess.Spec.SubjectAlternativeNames) > 0 { hosts = append(hosts, routerAccess.Spec.SubjectAlternativeNames...) } + // reading SANs from server certificate + serverCertificateData := serverSecret.Data["tls.crt"] + serverCertificateBlk, _ := pem.Decode(serverCertificateData) + if serverCertificateBlk != nil { + serverCertificate, err := x509.ParseCertificate(serverCertificateBlk.Bytes) + if err == nil { + for _, dnsName := range serverCertificate.DNSNames { + if !slices.Contains(hosts, dnsName) { + hosts = append(hosts, dnsName) + } + } + for _, ipAddr := range serverCertificate.IPAddresses { + if !slices.Contains(hosts, ipAddr.String()) { + hosts = append(hosts, ipAddr.String()) + } + } + } + } for _, host := range hosts { tokens = append(tokens, createToken(host)) } diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go index 7b2ad3f46..99d40110f 100644 --- a/pkg/nonkube/common/fs_config_renderer.go +++ b/pkg/nonkube/common/fs_config_renderer.go @@ -26,6 +26,9 @@ var ( api.CertificatesClientPath, api.CertificatesServerPath, api.CertificatesLinkPath, + api.InputCertificatesCaPath, + api.InputCertificatesClientPath, + api.InputCertificatesServerPath, api.LoadedSiteStatePath, api.RuntimeSiteStatePath, api.RuntimeTokenPath, @@ -239,11 +242,15 @@ func (c *FileSystemConfigurationRenderer) createTokens(siteState *api.SiteState) certName = linkAccess.Spec.TlsCredentials } secretName := fmt.Sprintf("client-%s", certName) + serverSecret, err := c.loadCertAsSecret(siteState, "server", certName) + if err != nil { + return fmt.Errorf("unable to load server secret %s: %w", certName, err) + } secret, err := c.loadClientSecret(siteState, secretName) if err != nil { return fmt.Errorf("unable to load client secret %s: %v", secretName, err) } - routerTokens := api.CreateTokens(*linkAccess, *secret) + routerTokens := api.CreateTokens(*linkAccess, *serverSecret, *secret) // routerAccess is valid (inter-router and edge endpoints defined) if len(routerTokens) > 0 { tokens = append(tokens, routerTokens...) @@ -329,8 +336,17 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S continue } secret := certs.GenerateCASecret(name, certificate.Spec.Subject) + + ignoreExisting := true + userCaSecret, err := c.loadUserCertAsSecret(siteState, "ca", name) + if userCaSecret != nil && err == nil { + // override with user provided CA + ignoreExisting = false + secret = *userCaSecret + fmt.Printf("-> User provided CA found: %s\n", name) + } caPath := path.Join(outputPath, string(api.CertificatesCaPath), name) - err = writeSecretFilesIgnore(caPath, &secret, true) + err = writeSecretFilesIgnore(caPath, &secret, ignoreExisting) if err != nil { return err } @@ -359,6 +375,12 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S } else { continue } + userSecret, err := c.loadUserCertAsSecret(siteState, purpose, name) + if userSecret != nil && err == nil { + // override with user provided secret + secret = *userSecret + fmt.Printf("-> User provided %s certificate found: %s\n", purpose, name) + } certPath := path.Join(outputPath, "certificates", purpose, name) err = writeSecretFiles(certPath, &secret) if err != nil { @@ -424,7 +446,36 @@ func (c *FileSystemConfigurationRenderer) loadClientSecret(siteState *api.SiteSt func (c *FileSystemConfigurationRenderer) loadCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) { outputPath := c.GetOutputPath(siteState) - certPath := path.Join(outputPath, fmt.Sprintf("certificates/%s", purpose), name) + return c.loadCertAsSecretFrom(outputPath, siteState, purpose, name) +} + +func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) { + userInputPath := path.Join(c.GetOutputPath(siteState), "input") + secret, err := c.loadCertAsSecretFrom(userInputPath, siteState, purpose, name) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + keys := map[string]bool{ + "ca.crt": false, + "tls.key": false, + "tls.crt": false, + } + for key := range secret.Data { + keys[key] = true + } + for _, hasKey := range keys { + if !hasKey { + return nil, fmt.Errorf("secret %q does not contain required keys: %v", name, keys) + } + } + return secret, nil +} + +func (c *FileSystemConfigurationRenderer) loadCertAsSecretFrom(basePath string, siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) { + certPath := path.Join(basePath, fmt.Sprintf("certificates/%s", purpose), name) var secret *corev1.Secret certDir, err := os.Open(certPath) if err != nil { From dce455fd7ddc3ea4744bfd3d06d2cf0ffe78f83c Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Fri, 27 Sep 2024 16:52:35 -0300 Subject: [PATCH 02/16] Updated bundle installation message --- internal/nonkube/bundle/install.sh.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index 44cc30c07..da249fc03 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -377,7 +377,7 @@ update_static_links() { for subject in ${subjects}; do link_file="${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" [ -f "${link_file}" ] && continue - echo "creating static link for new server subject found: ${subject}" + echo "- Creating static link for new server subject found: ${subject}" cp "${DIR}/runtime/link/link-${ra_name}-${bind_host:-127.0.0.1}.yaml" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" sed -i "s/host: .*/host: ${subject}/g" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" done From 73149c7fb8c3a161839eaf018b13d8415ed8be32 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Mon, 30 Sep 2024 08:58:35 -0300 Subject: [PATCH 03/16] Filtering just IP addresses and DNS names in SANs --- internal/nonkube/bundle/install.sh.template | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index da249fc03..610d00481 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -372,7 +372,8 @@ update_static_links() { # discovering SANS server_cert="${DIR}/certificates/server/${tls_credential:-${ra_name}}/tls.crt" [ ! -f "${server_cert}" ] && continue - subjects="$(openssl x509 -noout -ext subjectAltName -in "${server_cert}" |grep -v 'Subject Alternative Name:' | sed 's/ //g;s/,/\n/g' | awk -F: '{print $2}')" + subjects="$(openssl x509 -noout -ext subjectAltName -in "tls.crt" |grep -v 'Subject Alternative Name:' | \ + sed 's/ //g;s/,/\n/g' | awk -F: '{if ($1 == "DNS" || $1 == "IPAddress") print $2}')" [ -z "${subjects}" ] && continue for subject in ${subjects}; do link_file="${DIR}/runtime/link/link-${ra_name}-${subject}.yaml" From 494c7ec33adad2f4014c0c8098cc0c78665e64e4 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Mon, 30 Sep 2024 12:59:42 -0300 Subject: [PATCH 04/16] Splitted update of client credentials from link generation --- internal/nonkube/bundle/install.sh.template | 73 +++++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index 610d00481..d53abcd37 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -147,6 +147,9 @@ create_service() { } remove_service() { + # if systemd is not available, skip it + ${SYSTEMCTL} list-units > /dev/null 2>&1 || return + service="skupper-${NAMESPACE}.service" ${SYSTEMCTL} stop "${service}" ${SYSTEMCTL} disable "${service}" @@ -282,7 +285,7 @@ create_site() { sanity_check() { required_fields="SITE_NAME SOURCE_NAMESPACE NAMESPACE SKUPPER_OUTPUT_PATH SERVICE_DIR NAMESPACES_PATH SKUPPER_PLATFORM" - required_commands="python sed find grep wc xargs tar getent id ${PLATFORM_COMMAND}" + required_commands="python sed find grep wc xargs tar getent echo cp id cut ls rm mkdir ${PLATFORM_COMMAND}" for field_name in ${required_fields}; do eval [ -n "\${${field_name}}" ] || exit_error "Internal error: required field ${field_name} not defined" @@ -294,20 +297,28 @@ sanity_check() { done } -copy_user_certificates() { +list_valid_certificates() { + base_path="${1}" + cd "${base_path}" + for dir in $(find ./* -type d | cut -c 3-); do + [ -f "${dir}/tls.key" ] && [ -f "${dir}/tls.crt" ] && [ -f "${dir}/ca.crt" ] && echo "$dir" + done +} + +handle_provided_certificates() { export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates" [ ! -d "${USER_CERTIFICATES_PATH}" ] && return cas="" clients="" servers="" - if [ -d "${USER_CERTIFICATES_PATH}"/ca ]; then - cas=$(cd "${USER_CERTIFICATES_PATH}"/ca; ls -1) + if [ -d "${USER_CERTIFICATES_PATH}/ca" ]; then + cas=$(list_valid_certificates "${USER_CERTIFICATES_PATH}/ca") fi - if [ -d "${USER_CERTIFICATES_PATH}"/client ]; then - clients=$(cd "${USER_CERTIFICATES_PATH}"/client; ls -1) + if [ -d "${USER_CERTIFICATES_PATH}/client" ]; then + clients=$(list_valid_certificates "${USER_CERTIFICATES_PATH}/client") fi - if [ -d "${USER_CERTIFICATES_PATH}"/server ]; then - servers=$(cd "${USER_CERTIFICATES_PATH}"/server; ls -1) + if [ -d "${USER_CERTIFICATES_PATH}/server" ]; then + servers=$(list_valid_certificates "${USER_CERTIFICATES_PATH}/server") fi if [ -z "${cas}" ] && [ -z "${clients}" ] && [ -z "${servers}" ]; then return @@ -333,46 +344,66 @@ copy_user_certificates() { fi cp -r "${USER_CERTIFICATES_PATH}"/* "./${SOURCE_NAMESPACE}/certificates" update_static_links + generate_static_links_for_sans } update_static_links() { DIR="./${SOURCE_NAMESPACE}" export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates" router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f) + for ra in ${router_access_files}; do ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g') tls_credential=$(grep tlsCredentials: "${ra}" | awk '{print $NF}' | sed 's/"//g') bind_host=$(grep bindHost: "${ra}" | awk '{print $NF}' | sed 's/"//g') - input_cert_path="${USER_CERTIFICATES_PATH}/client/client-${tls_credential:-${ra_name}}" - [ ! -d "${input_cert_path}" ] && continue + client_cert_path="${USER_CERTIFICATES_PATH}/client/client-${tls_credential:-${ra_name}}" + + # no client certificate provided, skip + [ ! -d "${client_cert_path}" ] && continue + found=true for file in tls.key tls.crt ca.crt; do - if [ ! -f "${input_cert_path}/${file}" ]; then + if [ ! -f "${client_cert_path}/${file}" ]; then found=false break fi done ! ${found} && continue + # client certs found, reading client certificates - ca_crt=$(base64 -w0 "${input_cert_path}/ca.crt") - tls_crt=$(base64 -w0 "${input_cert_path}/tls.crt") - tls_key=$(base64 -w0 "${input_cert_path}/tls.key") + ca_crt=$(base64 -w0 "${client_cert_path}/ca.crt") + tls_crt=$(base64 -w0 "${client_cert_path}/tls.crt") + tls_key=$(base64 -w0 "${client_cert_path}/tls.key") # find all static links generated for the given RouterAccess static_link_files=$(find "${DIR:?}/runtime/link" -type f -name "link-${ra_name}-*") for link in ${static_link_files}; do - echo "- Updating credentials on static link: ${link}" + echo "- Updating client credentials on static link: ${link}" sed -i "s/tls.key: .*/tls.key: ${tls_key}/g" "${link}" sed -i "s/tls.crt: .*/tls.crt: ${tls_crt}/g" "${link}" sed -i "s/ca.crt: .*/ca.crt: ${ca_crt}/g" "${link}" done - # if openssl is present, discover SANs and create missing static links - if ! command -v openssl > /dev/null 2>&1; then - continue - fi + done +} + +generate_static_links_for_sans() { + DIR="./${SOURCE_NAMESPACE}" + + # if openssl is present, discover SANs and create missing static links + if ! command -v openssl > /dev/null 2>&1; then + return + fi + + router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f) + + for ra in ${router_access_files}; do + ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g') + tls_credential=$(grep tlsCredentials: "${ra}" | awk '{print $NF}' | sed 's/"//g') + bind_host=$(grep bindHost: "${ra}" | awk '{print $NF}' | sed 's/"//g') + # discovering SANS server_cert="${DIR}/certificates/server/${tls_credential:-${ra_name}}/tls.crt" [ ! -f "${server_cert}" ] && continue - subjects="$(openssl x509 -noout -ext subjectAltName -in "tls.crt" |grep -v 'Subject Alternative Name:' | \ + subjects="$(openssl x509 -noout -ext subjectAltName -in "${server_cert}" |grep -v 'Subject Alternative Name:' | \ sed 's/ //g;s/,/\n/g' | awk -F: '{if ($1 == "DNS" || $1 == "IPAddress") print $2}')" [ -z "${subjects}" ] && continue for subject in ${subjects}; do @@ -391,7 +422,7 @@ main() { sanity_check - copy_user_certificates + handle_provided_certificates if ${REMOVE}; then remove_site From c77113e12f190e058a47c135fb10e4905ed6d9ba Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Tue, 1 Oct 2024 11:35:52 -0300 Subject: [PATCH 05/16] Minor usability tweaks --- internal/nonkube/bundle/install.sh.template | 8 +++++--- pkg/nonkube/api/token.go | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index d53abcd37..acbf4d862 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -422,13 +422,15 @@ main() { sanity_check - handle_provided_certificates - if ${REMOVE}; then remove_site remove_service return - elif ${DUMP_TOKENS}; then + fi + + handle_provided_certificates + + if ${DUMP_TOKENS}; then dump_tokens return fi diff --git a/pkg/nonkube/api/token.go b/pkg/nonkube/api/token.go index 13c0532d0..e598dc453 100644 --- a/pkg/nonkube/api/token.go +++ b/pkg/nonkube/api/token.go @@ -105,9 +105,6 @@ func CreateTokens(routerAccess v1alpha1.RouterAccess, serverSecret v1.Secret, cl } var hosts []string hosts = append(hosts, utils.DefaultStr(routerAccess.Spec.BindHost, "127.0.0.1")) - if len(routerAccess.Spec.SubjectAlternativeNames) > 0 { - hosts = append(hosts, routerAccess.Spec.SubjectAlternativeNames...) - } // reading SANs from server certificate serverCertificateData := serverSecret.Data["tls.crt"] serverCertificateBlk, _ := pem.Decode(serverCertificateData) @@ -126,6 +123,12 @@ func CreateTokens(routerAccess v1alpha1.RouterAccess, serverSecret v1.Secret, cl } } } + // if no server certificate provided, use routerAccess.spec.subjectAlternativeNames + if len(hosts) == 1 { + if len(routerAccess.Spec.SubjectAlternativeNames) > 0 { + hosts = append(hosts, routerAccess.Spec.SubjectAlternativeNames...) + } + } for _, host := range hosts { tokens = append(tokens, createToken(host)) } From 2628db3a8226387ffb4da08af4c727a3f67e4f2e Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Tue, 1 Oct 2024 11:36:02 -0300 Subject: [PATCH 06/16] User provided certificates readme --- cmd/bootstrap/PROVIDED_CERTIFICATES.md | 88 ++++++++++++++++++++++++++ cmd/bootstrap/README.md | 8 +++ 2 files changed, 96 insertions(+) create mode 100644 cmd/bootstrap/PROVIDED_CERTIFICATES.md diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md new file mode 100644 index 000000000..42fbe4492 --- /dev/null +++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md @@ -0,0 +1,88 @@ +# User provided certificates + +Users can provide their own certificates to be used with Skupper V2 in non kube sites +during the bootstrap of a local site, when preparing a site bundle and even while installing +a site bundle at a remote machine. + +## Certificate Authorities (CAs) + +Certificate Authorities (CAs) can be provided at the time a local site is initialized +or a site bundle is being prepared. After a local site is created or a bundle is produced, +CAs won't be used to sign certificates, unless an active namespace is re-initialized. + +As an example, if you want Skupper to use your own CA certificates to generate and sign server +and client certificates used for site linking, you can simply create the following structure under +the namespace home of your choice, for example: + +```shell +${HOME}/.local/share/skupper/namespaces/default/input/certificates/ +└── ca + └── skupper-site-ca + ├── ca.crt + ├── tls.crt + └── tls.key +``` + +With that if you bootstrap a site to run in the default namespace, the CA certificates above will be +used to generate the server and client certificates for site linking. + +_**Note:**_ if you are preparing a bundle and want it to include your provided CA certificates, the +path to store them would be similar to the one mentioned above, but instead of using the `namespaces` +directory you should use the `bundles` directory, as in the following example: + +```shell +${HOME}/.local/share/skupper/bundles/default/input/certificates/ +``` + +## Server and Client certificates (for site linking) + +Server and client certificates can also be provided to help with site linking. + +When a local site is initialized, a bundle is being prepared or installed, Skupper will +inspect the Subject Alternative Names (SANs) from the provided server certificate, and +it will generate a static link for each of the entries, so that they can be distributed +to the appropriate client sites for site linking. + +The expected names for the server and client certificate pairs, is determined based on the +values of `RouterAccess.spec.tlsCredentials` (optional field), or `RouterAccess.name` (default). + +Supposing the value of `RouterAccess.spec.tlsCredentials` or `RouterAccess.name` (when the tlsCredentials +field is omitted) is `my-router-access`, then the following structure, for server and client certificates, +can be provided: + +```shell +${HOME}/.local/share/skupper/namespaces/default/input/certificates/ +├── client +│ └── client-my-router-access +│ ├── ca.crt +│ ├── tls.crt +│ └── tls.key +└── server + └── my-router-access + ├── ca.crt + ├── tls.crt + └── tls.key +``` + +At bootstrap or bundle installation times, you should see a message saying that the +user provided server and client certificates have been found. + +As an example, inspecting the subject alternative names of the provided server certificate above, +and supposing it is valid for the following domain name: + +```shell +X509v3 Subject Alternative Name: + DNS:my.local.server.com +``` + +If the following domain name is not defined as being the `spec.bindHost` or as part of the +`spec.subjectAlternativeNames` list of the `RouterAccess` resource, Skupper will also create a static +link that uses `my.local.server.com` as the target endpoint at: + +```shell +$HOME/.local/share/skupper/namespaces/default/runtime/link/link-my-router-access-my.local.server.com.yaml +``` + +If the respective server certificates are defined at bundle installation time, Skupper will also inspect +the subject alternative names of the public server certificate and create the static links for each domain +name and ip address found, only if the `openssl` binary is available. diff --git a/cmd/bootstrap/README.md b/cmd/bootstrap/README.md index b53f19ab3..db035a2e2 100644 --- a/cmd/bootstrap/README.md +++ b/cmd/bootstrap/README.md @@ -189,6 +189,14 @@ If the namespace is omitted, the "default" namespace is used. ./cmd/bootstrap/remove.sh [namespace] ``` +## Using custom certificates + +Users can provide their own certificates to be used when initializing a local site, +when preparing a site bundle to be installed somewhere else and even during a site +bundle installation time. + +More about user provided certificates can be found [here](PROVIDED_CERTIFICATES.md). + ## Example Here is a very basic demonstration on how you can run the `Hello World` example From 28e57ae2c834b8f454bd7900f07afd498814bcc5 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Wed, 2 Oct 2024 10:42:18 -0300 Subject: [PATCH 07/16] Added unit tests --- pkg/nonkube/api/token.go | 12 +- pkg/nonkube/api/token_test.go | 220 ++++++++++++++++++ pkg/nonkube/common/fs_config_renderer_test.go | 76 +++++- 3 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 pkg/nonkube/api/token_test.go diff --git a/pkg/nonkube/api/token.go b/pkg/nonkube/api/token.go index e598dc453..ac6c68b0b 100644 --- a/pkg/nonkube/api/token.go +++ b/pkg/nonkube/api/token.go @@ -111,16 +111,16 @@ func CreateTokens(routerAccess v1alpha1.RouterAccess, serverSecret v1.Secret, cl if serverCertificateBlk != nil { serverCertificate, err := x509.ParseCertificate(serverCertificateBlk.Bytes) if err == nil { - for _, dnsName := range serverCertificate.DNSNames { - if !slices.Contains(hosts, dnsName) { - hosts = append(hosts, dnsName) - } - } for _, ipAddr := range serverCertificate.IPAddresses { - if !slices.Contains(hosts, ipAddr.String()) { + if ipAddr.String() != "" && !slices.Contains(hosts, ipAddr.String()) { hosts = append(hosts, ipAddr.String()) } } + for _, dnsName := range serverCertificate.DNSNames { + if dnsName != "" && !slices.Contains(hosts, dnsName) { + hosts = append(hosts, dnsName) + } + } } } // if no server certificate provided, use routerAccess.spec.subjectAlternativeNames diff --git a/pkg/nonkube/api/token_test.go b/pkg/nonkube/api/token_test.go new file mode 100644 index 000000000..d00c2a542 --- /dev/null +++ b/pkg/nonkube/api/token_test.go @@ -0,0 +1,220 @@ +package api + +import ( + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + "testing" + + "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1" + "github.com/skupperproject/skupper/pkg/certs" + "gotest.tools/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCreateTokens(t *testing.T) { + /* + * No inter-router nor edge roles in RouterAccess + * RouterAccess with only one of the roles + * RouterAccess with both roles + * Bad user provided server certificate (no tls.crt in data) + * Good user provided server certificate (no SANs) + * Good user provided server certificate with valid SANs + * subjectAlternativeNames (from routeraccess.spec to be ignored) + * Good user provided server certificate with empty SANs + * subjectAlternativeNames (from routeraccess.spec to be used) + */ + + var clientSecret = v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "link-fake-router-access", + }, + Data: map[string][]byte{ + "tls.key": []byte("fake-client-key"), + "tls.crt": []byte("fake-client-cert"), + "ca.crt": []byte("fake-client-ca"), + }, + } + + tests := []struct { + name string + ra v1alpha1.RouterAccess + serverSecret v1.Secret + expectedHosts []string + }{ + { + name: "no-inter-router-or-edge-roles", + ra: fakeRouterAccessNoInterRouterEdgeRoles(), + expectedHosts: nil, + }, + { + name: "inter-router-role-only", + ra: fakeRouterAccessInterRouterRole(), + expectedHosts: []string{ + "127.0.0.1", + }, + }, + { + name: "both-roles", + ra: fakeRouterAccessBothRoles(), + expectedHosts: []string{ + "127.0.0.1", + }, + }, + { + name: "invalid-server-cert", + ra: fakeRouterAccessBothRoles(), + serverSecret: fakeServerSecretBad(), + expectedHosts: []string{ + "127.0.0.1", + }, + }, + { + name: "server-cert-no-hosts", + ra: fakeRouterAccessBothRoles(), + serverSecret: fakeServerSecret([]string{}), + expectedHosts: []string{ + "127.0.0.1", + }, + }, + { + name: "sans-provided-no-server-cert", + ra: fakeRouterAccessWithSANs("fake.host.one", "fake.host.two"), + expectedHosts: []string{ + "127.0.0.1", + "fake.host.one", + "fake.host.two", + }, + }, + { + name: "sans-provided-empty-server-cert", + ra: fakeRouterAccessWithSANs("fake.host.one", "fake.host.two"), + serverSecret: fakeServerSecret([]string{}), + expectedHosts: []string{ + "127.0.0.1", + "fake.host.one", + "fake.host.two", + }, + }, + { + name: "sans-provided-server-cert-with-hosts", + ra: fakeRouterAccessWithSANs("fake.host.one", "fake.host.two"), + serverSecret: fakeServerSecret([]string{"server.host.one", "server.host.two"}), + expectedHosts: []string{ + "127.0.0.1", + "server.host.one", + "server.host.two", + }, + }, + { + name: "sans-provided-server-cert-with-hosts-and-ips", + ra: fakeRouterAccessWithSANs("fake.host.one", "fake.host.two"), + serverSecret: fakeServerSecret([]string{"server.host.one", "server.host.two", "10.0.0.1", "10.0.0.2"}), + expectedHosts: []string{ + "127.0.0.1", + "10.0.0.1", + "10.0.0.2", + "server.host.one", + "server.host.two", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tokens := CreateTokens(test.ra, test.serverSecret, clientSecret) + // validate expected token count + tokensJson, _ := json.MarshalIndent(tokens, "", " ") + assert.Equal(t, len(tokens), len(test.expectedHosts), "expected tokens for hosts: %v - got: %v", test.expectedHosts, string(tokensJson)) + // nothing else to validate + if len(test.expectedHosts) == 0 { + return + } + hostsFound := map[string]bool{} + for _, host := range test.expectedHosts { + hostsFound[host] = false + } + // validate tokens + for _, token := range tokens { + assert.Equal(t, token.Links[0].Name, fmt.Sprintf("link-%s", test.ra.Name)) + assert.Equal(t, token.Links[0].Spec.Cost, 1) + assert.Equal(t, token.Links[0].Spec.TlsCredentials, clientSecret.Name) + assert.Equal(t, len(token.Links[0].Spec.Endpoints), len(test.ra.Spec.Roles)) + var raRolesPorts = make(map[string]string) + for _, role := range test.ra.Spec.Roles { + raRolesPorts[role.Name] = strconv.Itoa(role.Port) + } + for _, endpoint := range token.Links[0].Spec.Endpoints { + assert.Equal(t, endpoint.Port, raRolesPorts[endpoint.Name]) + assert.Assert(t, slices.Contains(test.expectedHosts, endpoint.Host), + "endpoint host %q not expected in %v", endpoint.Host, test.expectedHosts) + hostsFound[endpoint.Host] = true + } + assert.Equal(t, token.Secret.Name, clientSecret.Name) + } + for _, found := range hostsFound { + assert.Assert(t, found, "not all hosts found: %v", hostsFound) + } + }) + } +} + +func fakeRouterAccess() v1alpha1.RouterAccess { + var ra v1alpha1.RouterAccess + ra.Name = "fake-router-access" + return ra +} + +func fakeRouterAccessNoInterRouterEdgeRoles() v1alpha1.RouterAccess { + var ra = fakeRouterAccess() + ra.Spec.Roles = []v1alpha1.RouterAccessRole{ + { + Name: "normal", + Port: 5671, + }, + } + return ra +} + +func fakeRouterAccessInterRouterRole() v1alpha1.RouterAccess { + var ra = fakeRouterAccess() + ra.Spec.Roles = []v1alpha1.RouterAccessRole{ + { + Name: "inter-router", + Port: 55671, + }, + } + return ra +} + +func fakeRouterAccessBothRoles() v1alpha1.RouterAccess { + var ra = fakeRouterAccessInterRouterRole() + ra.Spec.Roles = append(ra.Spec.Roles, v1alpha1.RouterAccessRole{ + Name: "edge", + Port: 45671, + }) + return ra +} + +func fakeRouterAccessWithSANs(sans ...string) v1alpha1.RouterAccess { + var ra = fakeRouterAccessBothRoles() + ra.Spec.SubjectAlternativeNames = sans + return ra +} + +func fakeServerSecretBad() v1.Secret { + ca := certs.GenerateCASecret("fake-ca", "fake-ca") + server := certs.GenerateSecret("fake-server-cert", "fake-server-cert", "", &ca) + delete(server.Data, "tls.crt") + return server +} + +func fakeServerSecret(hosts []string) v1.Secret { + hostsCsv := strings.Join(hosts, ",") + ca := certs.GenerateCASecret("fake-ca", "fake-ca") + server := certs.GenerateSecret("fake-server-cert", "fake-server-cert", hostsCsv, &ca) + return server +} diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go index 3c9791c7e..6691fc5ec 100644 --- a/pkg/nonkube/common/fs_config_renderer_test.go +++ b/pkg/nonkube/common/fs_config_renderer_test.go @@ -1,12 +1,14 @@ package common import ( + "bytes" "os" "path" "testing" "github.com/skupperproject/skupper/api/types" "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1" + "github.com/skupperproject/skupper/pkg/certs" "github.com/skupperproject/skupper/pkg/nonkube/api" "gotest.tools/assert" corev1 "k8s.io/api/core/v1" @@ -14,6 +16,14 @@ import ( ) func TestFileSystemConfigurationRenderer_Render(t *testing.T) { + testFileSystemConfigurationRendererRender(t, false) +} + +func TestFileSystemConfigurationRendererWithInputCertificates_Render(t *testing.T) { + testFileSystemConfigurationRendererRender(t, true) +} + +func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificates bool) { ss := fakeSiteState() ss.CreateLinkAccessesCertificates() ss.CreateBridgeCertificates() @@ -23,6 +33,9 @@ func TestFileSystemConfigurationRenderer_Render(t *testing.T) { err := os.RemoveAll(customOutputPath) assert.Assert(t, err) }() + if addInputCertificates { + createInputCertificates(t, customOutputPath) + } fsConfigRenderer := new(FileSystemConfigurationRenderer) fsConfigRenderer.customOutputPath = customOutputPath assert.Assert(t, fsConfigRenderer.Render(ss)) @@ -61,7 +74,13 @@ func TestFileSystemConfigurationRenderer_Render(t *testing.T) { "certificates/link/link-one-profile/tls.key", "runtime/state/platform.yaml", "runtime/link/link-link-access-one-127.0.0.1.yaml", - "runtime/link/link-link-access-one-localhost.yaml", + } + if !addInputCertificates { + expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-localhost.yaml") + } else { + expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-10.0.0.1.yaml") + expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-10.0.0.2.yaml") + expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-fake.domain.yaml") } for _, fileName := range expectedFiles { fs, err := os.Stat(path.Join(customOutputPath, fileName)) @@ -69,6 +88,61 @@ func TestFileSystemConfigurationRenderer_Render(t *testing.T) { assert.Assert(t, fs.Mode().IsRegular()) assert.Assert(t, fs.Size() > 0) } + if addInputCertificates { + compareCertificates(t, customOutputPath) + } +} + +func compareCertificates(t *testing.T, customOutputPath string) { + caPath := path.Join(customOutputPath, "certificates/ca/skupper-site-ca") + serverPath := path.Join(customOutputPath, "certificates/server/link-access-one") + clientPath := path.Join(customOutputPath, "certificates/client/client-link-access-one") + inputCaPath := path.Join(customOutputPath, "input/certificates/ca/skupper-site-ca") + inputServerPath := path.Join(customOutputPath, "input/certificates/server/link-access-one") + inputClientPath := path.Join(customOutputPath, "input/certificates/client/client-link-access-one") + pathsToCompare := map[string]string{ + caPath: inputCaPath, + serverPath: inputServerPath, + clientPath: inputClientPath, + } + for certPath, inputCertPath := range pathsToCompare { + entries, err := os.ReadDir(certPath) + assert.Assert(t, err) + assert.Assert(t, len(entries) == 3) + for _, filename := range []string{"ca.crt", "tls.key", "tls.crt"} { + activeData, err := os.ReadFile(path.Join(certPath, filename)) + assert.Assert(t, err) + inputData, err := os.ReadFile(path.Join(inputCertPath, filename)) + assert.Assert(t, err) + assert.Assert(t, bytes.Equal(activeData, inputData)) + } + } +} + +func createInputCertificates(t *testing.T, customOutputPath string) { + // preparing certificates + fakeHosts := "10.0.0.1,10.0.0.2,fake.domain" + ca := certs.GenerateCASecret("fake-ca", "fake-ca") + server := certs.GenerateSecret("fake-server-cert", "fake-server-cert", fakeHosts, &ca) + client := certs.GenerateSecret("fake-client-cert", "fake-client-cert", "", &ca) + + // paths for each provided certificate + caPath := path.Join(customOutputPath, "namespaces/default/input/certificates/ca/skupper-site-ca") + serverPath := path.Join(customOutputPath, "namespaces/default/input/certificates/server/link-access-one") + clientPath := path.Join(customOutputPath, "namespaces/default/input/certificates/client/client-link-access-one") + certsMap := map[string]corev1.Secret{ + caPath: ca, + serverPath: server, + clientPath: client, + } + + // writing certificates to disk + for certPath, secret := range certsMap { + assert.Assert(t, os.MkdirAll(certPath, 0755)) + for filename, data := range secret.Data { + assert.Assert(t, os.WriteFile(path.Join(certPath, filename), data, 0644)) + } + } } func fakeSiteState() *api.SiteState { From 6bc52c1f25d60012297e2c03002608964885d8c3 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Wed, 2 Oct 2024 13:44:03 -0300 Subject: [PATCH 08/16] Separated input sources from loaded sources --- cmd/bootstrap/PROVIDED_CERTIFICATES.md | 4 ++-- cmd/bootstrap/README.md | 2 +- cmd/bootstrap/bootstrap.go | 6 +++--- internal/nonkube/bundle/install.sh.template | 2 +- internal/nonkube/client/fs/path_provider.go | 4 ++-- pkg/nonkube/api/environment.go | 1 + pkg/nonkube/common/fs_config_renderer.go | 21 ++++++++------------- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md index 42fbe4492..f91d73296 100644 --- a/cmd/bootstrap/PROVIDED_CERTIFICATES.md +++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md @@ -43,12 +43,12 @@ inspect the Subject Alternative Names (SANs) from the provided server certificat it will generate a static link for each of the entries, so that they can be distributed to the appropriate client sites for site linking. -The expected names for the server and client certificate pairs, is determined based on the +The expected directory names for the server and client certificates, is determined based on the values of `RouterAccess.spec.tlsCredentials` (optional field), or `RouterAccess.name` (default). Supposing the value of `RouterAccess.spec.tlsCredentials` or `RouterAccess.name` (when the tlsCredentials field is omitted) is `my-router-access`, then the following structure, for server and client certificates, -can be provided: +must be provided: ```shell ${HOME}/.local/share/skupper/namespaces/default/input/certificates/ diff --git a/cmd/bootstrap/README.md b/cmd/bootstrap/README.md index db035a2e2..da7ba160c 100644 --- a/cmd/bootstrap/README.md +++ b/cmd/bootstrap/README.md @@ -381,7 +381,7 @@ in your browser and it should work. ### Updating an existing installation Suppose modifications have been made to the `west` site CRs, directly at the -namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/sources). +namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/input/sources). To re-initialize the west site, run: diff --git a/cmd/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go index 4992d067a..c8f6a379e 100644 --- a/cmd/bootstrap/bootstrap.go +++ b/cmd/bootstrap/bootstrap.go @@ -152,7 +152,7 @@ func main() { } if inputPath == "" { // when input path is empty, but a namespace is provided, try to reload an existing site definition - existingPath := api.GetInternalOutputPath(namespace, api.LoadedSiteStatePath) + existingPath := api.GetInternalOutputPath(namespace, api.InputSiteStatePath) if _, err := os.Stat(existingPath); err == nil { inputPath = existingPath fmt.Printf("Sources will consumed from namespace %q\n", namespace) @@ -206,7 +206,7 @@ func main() { break } } - sourcesPath, _ := api.GetHostSiteInternalPath(siteState.Site, api.LoadedSiteStatePath) + sourcesPath, _ := api.GetHostSiteInternalPath(siteState.Site, api.InputSiteStatePath) fmt.Printf("Definition is available at: %s\n", sourcesPath) } else { siteHome, err := api.GetHostBundlesPath() @@ -227,7 +227,7 @@ func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api. var siteStateLoader api.SiteStateLoader var reloadExisting bool isBundle := bundleStrategy != "" - sourcesPath := api.GetInternalOutputPath(namespace, api.LoadedSiteStatePath) + sourcesPath := api.GetInternalOutputPath(namespace, api.InputSiteStatePath) _, err := os.Stat(sourcesPath) if !isBundle && err == nil { reloadExisting = true diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index acbf4d862..f6636a5e2 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -252,7 +252,7 @@ create_site() { echo "Namespace : ${NAMESPACE}" echo "Site name : ${SITE_NAME}" echo "Platform : ${SKUPPER_PLATFORM}" - echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/sources" + echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources" echo "Version : ${VERSION}" # Create base directory tree diff --git a/internal/nonkube/client/fs/path_provider.go b/internal/nonkube/client/fs/path_provider.go index 45b952c7d..5851fdf08 100644 --- a/internal/nonkube/client/fs/path_provider.go +++ b/internal/nonkube/client/fs/path_provider.go @@ -7,7 +7,7 @@ type PathProvider struct { } func (p *PathProvider) getDefaultNamespace() string { - return ".local/share/skupper/namespaces/default/sources" + return ".local/share/skupper/namespaces/default/input/sources" } func (p *PathProvider) GetNamespace() string { @@ -15,5 +15,5 @@ func (p *PathProvider) GetNamespace() string { if p.Namespace == "" { return p.getDefaultNamespace() } - return fmt.Sprintf(".local/share/skupper/namespaces/%s/sources", p.Namespace) + return fmt.Sprintf(".local/share/skupper/namespaces/%s/input/sources", p.Namespace) } diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go index 2902d8eed..2e874cfb9 100644 --- a/pkg/nonkube/api/environment.go +++ b/pkg/nonkube/api/environment.go @@ -20,6 +20,7 @@ const ( InputCertificatesCaPath InternalPath = "input/certificates/ca" InputCertificatesClientPath InternalPath = "input/certificates/client" InputCertificatesServerPath InternalPath = "input/certificates/server" + InputSiteStatePath InternalPath = "input/sources" LoadedSiteStatePath InternalPath = "sources" RuntimeSiteStatePath InternalPath = "runtime/state" RuntimeTokenPath InternalPath = "runtime/link" diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go index 99d40110f..adba506cf 100644 --- a/pkg/nonkube/common/fs_config_renderer.go +++ b/pkg/nonkube/common/fs_config_renderer.go @@ -29,6 +29,7 @@ var ( api.InputCertificatesCaPath, api.InputCertificatesClientPath, api.InputCertificatesServerPath, + api.InputSiteStatePath, api.LoadedSiteStatePath, api.RuntimeSiteStatePath, api.RuntimeTokenPath, @@ -42,6 +43,7 @@ var ( api.RuntimeSiteStatePath, api.RuntimeTokenPath, api.RuntimeScriptsPath, + api.LoadedSiteStatePath, } ) @@ -154,32 +156,25 @@ func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, run if loadedSiteState != nil { outputPath := c.GetOutputPath(loadedSiteState) sourcesPath := path.Join(outputPath, string(api.LoadedSiteStatePath)) - existingSources, _ := new(utils.DirectoryReader).ReadDir(sourcesPath, nil) + inputSourcesPath := path.Join(outputPath, string(api.InputSiteStatePath)) + existingSources, _ := new(utils.DirectoryReader).ReadDir(inputSourcesPath, nil) // when sources are already defined, we back them up if len(existingSources) > 0 { tb := utils.NewTarball() - err := tb.AddFiles(sourcesPath) + err := tb.AddFiles(inputSourcesPath) if err != nil { return fmt.Errorf("unable to backup existing sources: %s", err) } - tbFile := path.Join(outputPath, "sources.backup.tar.gz") + tbFile := path.Join(outputPath, "input", "sources.backup.tar.gz") err = tb.Save(tbFile) if err != nil { return fmt.Errorf("unable to backup sources.backup.tar.gz: %s", err) } err = os.RemoveAll(sourcesPath) if err != nil { - return fmt.Errorf("unable to remove old sources: %s", err) + return fmt.Errorf("unable to remove former loaded state: %s", err) } if err = os.Mkdir(sourcesPath, 0755); err != nil { - defer func() { - if err != nil { - if restoreErr := tb.Extract(tbFile, outputPath); restoreErr != nil { - fmt.Printf("unable to restore sources.backup.tar.gz: %s", restoreErr) - return - } - } - }() return fmt.Errorf("unable to recreate sources directory %s: %s", sourcesPath, err) } } @@ -450,7 +445,7 @@ func (c *FileSystemConfigurationRenderer) loadCertAsSecret(siteState *api.SiteSt } func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) { - userInputPath := path.Join(c.GetOutputPath(siteState), "input") + userInputPath := path.Join(api.GetDefaultOutputPath(siteState.GetNamespace()), "input") secret, err := c.loadCertAsSecretFrom(userInputPath, siteState, purpose, name) if err != nil { if os.IsNotExist(err) { From 062f5cb87a830283918903c186f54f250467f627 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Wed, 2 Oct 2024 14:47:42 -0300 Subject: [PATCH 09/16] Fixed approach to reading input path --- pkg/nonkube/common/fs_config_renderer.go | 11 ++++++++++- pkg/nonkube/common/fs_config_renderer_test.go | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go index adba506cf..3ebb0f767 100644 --- a/pkg/nonkube/common/fs_config_renderer.go +++ b/pkg/nonkube/common/fs_config_renderer.go @@ -152,6 +152,15 @@ func (c *FileSystemConfigurationRenderer) GetOutputPath(siteState *api.SiteState return defaultOutputPathProvider(siteState.Site.Namespace) } +func (c *FileSystemConfigurationRenderer) GetInputPath(siteState *api.SiteState) string { + var customSiteHomeProvider = api.GetCustomSiteHome + var defaultOutputPathProvider = api.GetDefaultOutputPath + if c.customOutputPath != "" { + return path.Join(customSiteHomeProvider(siteState.Site, c.customOutputPath), "input") + } + return path.Join(defaultOutputPathProvider(siteState.Site.Namespace), "input") +} + func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, runtimeSiteState *api.SiteState) error { if loadedSiteState != nil { outputPath := c.GetOutputPath(loadedSiteState) @@ -445,7 +454,7 @@ func (c *FileSystemConfigurationRenderer) loadCertAsSecret(siteState *api.SiteSt } func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) { - userInputPath := path.Join(api.GetDefaultOutputPath(siteState.GetNamespace()), "input") + userInputPath := c.GetInputPath(siteState) secret, err := c.loadCertAsSecretFrom(userInputPath, siteState, purpose, name) if err != nil { if os.IsNotExist(err) { diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go index 6691fc5ec..1c38f225e 100644 --- a/pkg/nonkube/common/fs_config_renderer_test.go +++ b/pkg/nonkube/common/fs_config_renderer_test.go @@ -30,10 +30,11 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate customOutputPath, err := os.MkdirTemp("", "fs-config-renderer-*") assert.Assert(t, err) defer func() { - err := os.RemoveAll(customOutputPath) - assert.Assert(t, err) + //err := os.RemoveAll(customOutputPath) + //assert.Assert(t, err) }() if addInputCertificates { + t.Logf(customOutputPath) createInputCertificates(t, customOutputPath) } fsConfigRenderer := new(FileSystemConfigurationRenderer) From 5e77683b2a8b6a8ebd42eb248d41751ad7eb70f3 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Thu, 3 Oct 2024 10:51:45 -0300 Subject: [PATCH 10/16] Usability improvements --- cmd/bootstrap/bootstrap.go | 29 +++++++------ internal/nonkube/bundle/install.sh.template | 8 +++- pkg/nonkube/api/environment.go | 45 ++++++++++----------- pkg/nonkube/common/fs_config_renderer.go | 4 ++ pkg/nonkube/common/systemd.go | 5 +-- pkg/nonkube/compat/site_state_renderer.go | 5 +-- pkg/nonkube/systemd/site_state_renderer.go | 5 +-- 7 files changed, 53 insertions(+), 48 deletions(-) diff --git a/cmd/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go index c8f6a379e..ebfb872e2 100644 --- a/cmd/bootstrap/bootstrap.go +++ b/cmd/bootstrap/bootstrap.go @@ -10,6 +10,7 @@ import ( "github.com/skupperproject/skupper/api/types" internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle" + internalutils "github.com/skupperproject/skupper/internal/utils" "github.com/skupperproject/skupper/pkg/config" "github.com/skupperproject/skupper/pkg/nonkube/api" "github.com/skupperproject/skupper/pkg/nonkube/bundle" @@ -80,7 +81,6 @@ func main() { fmt.Println(version.Version) os.Exit(0) } - // if user overrides and force empty, use it as default if inputPath != "" { var err error inputPath, err = filepath.Abs(inputPath) @@ -150,16 +150,27 @@ func main() { if namespace == "" { namespace = "default" } + existingPath := api.GetInternalOutputPath(namespace, api.InputSiteStatePath) + inputSourcesDefined := false + if _, err := os.Stat(existingPath); err == nil { + dirReader := new(internalutils.DirectoryReader) + filesFound, _ := dirReader.ReadDir(existingPath, nil) + inputSourcesDefined = len(filesFound) > 0 + } if inputPath == "" { // when input path is empty, but a namespace is provided, try to reload an existing site definition - existingPath := api.GetInternalOutputPath(namespace, api.InputSiteStatePath) - if _, err := os.Stat(existingPath); err == nil { + if inputSourcesDefined { inputPath = existingPath fmt.Printf("Sources will consumed from namespace %q\n", namespace) } else { fmt.Printf("Input path has not been provided and namespace %s does not exist\n", namespace) + fmt.Printf("No sources found at: %s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath))) os.Exit(1) } + } else if inputSourcesDefined { + fmt.Printf("Input path has been provided, but namespace %s has input sources defined at:\n", namespace) + fmt.Printf("%s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath))) + os.Exit(1) } // if namespace already exists, fail if force is not set @@ -195,10 +206,7 @@ func main() { if !isBundle { fmt.Printf("Platform: %s\n", platform) tokenPath := api.GetInternalOutputPath(siteState.Site.Namespace, api.RuntimeTokenPath) - hostTokenPath, err := api.GetHostSiteInternalPath(siteState.Site, api.RuntimeTokenPath) - if err != nil { - fmt.Println("Failed to get site's static links path:", err) - } + hostTokenPath := api.GetHostSiteInternalPath(siteState.Site, api.RuntimeTokenPath) tokens, _ := os.ReadDir(tokenPath) for _, token := range tokens { if !token.IsDir() { @@ -206,13 +214,10 @@ func main() { break } } - sourcesPath, _ := api.GetHostSiteInternalPath(siteState.Site, api.InputSiteStatePath) + sourcesPath := api.GetHostSiteInternalPath(siteState.Site, api.InputSiteStatePath) fmt.Printf("Definition is available at: %s\n", sourcesPath) } else { - siteHome, err := api.GetHostBundlesPath() - if err != nil { - fmt.Println("Failed to get site bundle base directory:", err) - } + siteHome := api.GetHostBundlesPath() installationFile := path.Join(siteHome, fmt.Sprintf("skupper-install-%s.sh", siteState.Site.Name)) if internalbundle.GetBundleStrategy(bundleStrategy) == string(internalbundle.BundleStrategyTarball) { installationFile = path.Join(siteHome, fmt.Sprintf("skupper-install-%s.tar.gz", siteState.Site.Name)) diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template index f6636a5e2..4e279f876 100644 --- a/internal/nonkube/bundle/install.sh.template +++ b/internal/nonkube/bundle/install.sh.template @@ -242,12 +242,18 @@ create_containers() { } create_site() { - if [ -d "${NAMESPACES_PATH}/${NAMESPACE}/runtime" ]; then + if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime" ]; then echo "Failed: namespace \"${NAMESPACE}\" is already defined" echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}" exit 1 fi + if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources" ]; then + echo "Failed: namespace \"${NAMESPACE}\" already contains input sources" + echo "Location: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources" + exit 1 + fi + echo "Skupper site bundle installation" echo "Namespace : ${NAMESPACE}" echo "Site name : ${SITE_NAME}" diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go index 2e874cfb9..f2b990639 100644 --- a/pkg/nonkube/api/environment.go +++ b/pkg/nonkube/api/environment.go @@ -67,40 +67,42 @@ func GetRuntimeDir() string { // GetHostDataHome returns the value of the SKUPPER_OUTPUT_PATH environment // variable when running via container or the result of GetDataHome() otherwise. // This is only useful during the bootstrap process. -func GetHostDataHome() (string, error) { +func GetHostDataHome() string { // If container provides SKUPPER_OUTPUT_PATH use it if os.Getenv("SKUPPER_OUTPUT_PATH") != "" { - return os.Getenv("SKUPPER_OUTPUT_PATH"), nil + return os.Getenv("SKUPPER_OUTPUT_PATH") } - return GetDataHome(), nil + return GetDataHome() } -func GetHostSiteHome(site *v1alpha1.Site) (string, error) { - dataHome, err := GetHostDataHome() - if err != nil { - return "", err - } +func GetHostSiteHome(site *v1alpha1.Site) string { + dataHome := GetHostDataHome() ns := site.Namespace if ns == "" { ns = "default" } - return path.Join(dataHome, "namespaces", ns), nil + return path.Join(dataHome, "namespaces", ns) +} + +func GetHostNamespaceHome(ns string) string { + dataHome := GetHostDataHome() + if ns == "" { + ns = "default" + } + return path.Join(dataHome, "namespaces", ns) } -func GetHostNamespacesPath() (string, error) { +func GetHostNamespacesPath() string { return getHostPath("namespaces") } -func GetHostBundlesPath() (string, error) { +func GetHostBundlesPath() string { return getHostPath("bundles") } -func getHostPath(basePath string) (string, error) { - dataHome, err := GetHostDataHome() - if err != nil { - return "", err - } - return path.Join(dataHome, basePath), nil +func getHostPath(basePath string) string { + dataHome := GetHostDataHome() + return path.Join(dataHome, basePath) } func GetCustomSiteHome(site *v1alpha1.Site, customBaseDir string) string { @@ -119,12 +121,9 @@ func getCustomSiteHome(site *v1alpha1.Site, customBaseDir string, basePath strin return path.Join(customBaseDir, basePath, ns) } -func GetHostSiteInternalPath(site *v1alpha1.Site, internalPath InternalPath) (string, error) { - dataHome, err := GetHostSiteHome(site) - if err != nil { - return "", err - } - return path.Join(dataHome, string(internalPath)), nil +func GetHostSiteInternalPath(site *v1alpha1.Site, internalPath InternalPath) string { + dataHome := GetHostSiteHome(site) + return path.Join(dataHome, string(internalPath)) } func IsRunningInContainer() bool { diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go index 3ebb0f767..783903feb 100644 --- a/pkg/nonkube/common/fs_config_renderer.go +++ b/pkg/nonkube/common/fs_config_renderer.go @@ -186,6 +186,10 @@ func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, run if err = os.Mkdir(sourcesPath, 0755); err != nil { return fmt.Errorf("unable to recreate sources directory %s: %s", sourcesPath, err) } + } else { + if err := api.MarshalSiteState(*loadedSiteState, inputSourcesPath); err != nil { + return err + } } if err := api.MarshalSiteState(*loadedSiteState, sourcesPath); err != nil { return err diff --git a/pkg/nonkube/common/systemd.go b/pkg/nonkube/common/systemd.go index 66dca123b..f7b9755aa 100644 --- a/pkg/nonkube/common/systemd.go +++ b/pkg/nonkube/common/systemd.go @@ -50,10 +50,7 @@ type systemdServiceInfo struct { func NewSystemdServiceInfo(siteState *api.SiteState, platform string) (SystemdService, error) { site := siteState.Site - siteHomePath, err := api.GetHostSiteHome(site) - if err != nil { - return nil, err - } + siteHomePath := api.GetHostSiteHome(site) siteScriptPath := path.Join(siteHomePath, string(api.RuntimeScriptsPath)) siteConfigPath := path.Join(siteHomePath, string(api.ConfigRouterPath)) namespace := site.Namespace diff --git a/pkg/nonkube/compat/site_state_renderer.go b/pkg/nonkube/compat/site_state_renderer.go index 4a54850d2..9b250bb39 100644 --- a/pkg/nonkube/compat/site_state_renderer.go +++ b/pkg/nonkube/compat/site_state_renderer.go @@ -177,10 +177,7 @@ func (s *SiteStateRenderer) cleanupExistingNamespace(siteState *api.SiteState) e } func (s *SiteStateRenderer) prepareContainers() error { - siteConfigPath, err := api.GetHostSiteHome(s.siteState.Site) - if err != nil { - return err - } + siteConfigPath := api.GetHostSiteHome(s.siteState.Site) s.containers = make(map[string]container.Container) s.containers[types.RouterComponent] = container.Container{ Name: fmt.Sprintf("%s-skupper-router", s.siteState.GetNamespace()), diff --git a/pkg/nonkube/systemd/site_state_renderer.go b/pkg/nonkube/systemd/site_state_renderer.go index 124cd66a2..6e0af91a9 100644 --- a/pkg/nonkube/systemd/site_state_renderer.go +++ b/pkg/nonkube/systemd/site_state_renderer.go @@ -77,10 +77,7 @@ func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) s.siteState.CreateLinkAccessesCertificates() s.siteState.CreateBridgeCertificates() // rendering non-kube configuration files and certificates - siteHome, err := api.GetHostSiteHome(s.siteState.Site) - if err != nil { - return fmt.Errorf("failed to get site home: %w", err) - } + siteHome := api.GetHostSiteHome(s.siteState.Site) s.configRenderer = &common.FileSystemConfigurationRenderer{ SslProfileBasePath: siteHome, Platform: string(types.PlatformSystemd), From 49fd066e4b111355eeafd604cfadf3cf5d48340c Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Thu, 3 Oct 2024 10:55:33 -0300 Subject: [PATCH 11/16] Updated provided certificates readme --- cmd/bootstrap/PROVIDED_CERTIFICATES.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md index f91d73296..9b3fbe2f3 100644 --- a/cmd/bootstrap/PROVIDED_CERTIFICATES.md +++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md @@ -26,14 +26,6 @@ ${HOME}/.local/share/skupper/namespaces/default/input/certificates/ With that if you bootstrap a site to run in the default namespace, the CA certificates above will be used to generate the server and client certificates for site linking. -_**Note:**_ if you are preparing a bundle and want it to include your provided CA certificates, the -path to store them would be similar to the one mentioned above, but instead of using the `namespaces` -directory you should use the `bundles` directory, as in the following example: - -```shell -${HOME}/.local/share/skupper/bundles/default/input/certificates/ -``` - ## Server and Client certificates (for site linking) Server and client certificates can also be provided to help with site linking. From 4f41807338d440c7a1ef6e5cde09e2d37f6c17ed Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Thu, 3 Oct 2024 11:16:24 -0300 Subject: [PATCH 12/16] Fixed unit test failing --- pkg/nonkube/api/environment_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/nonkube/api/environment_test.go b/pkg/nonkube/api/environment_test.go index 649e36ee3..01d3715a4 100644 --- a/pkg/nonkube/api/environment_test.go +++ b/pkg/nonkube/api/environment_test.go @@ -411,8 +411,7 @@ func TestGetHostSiteHome(t *testing.T) { if scenario.useXdgDataHome { t.Setenv(envXdgDataHome, fakeXdgDataHome) } - siteHome, err := GetHostSiteHome(fakeSite) - assert.Assert(t, err) + siteHome := GetHostSiteHome(fakeSite) assert.Equal(t, siteHome, scenario.expectedSiteHome) } } From fe744d8f13f75c861cad27ce090e39b36f6bd6de Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Thu, 3 Oct 2024 11:57:45 -0300 Subject: [PATCH 13/16] fixup! Fixed unit test failing --- pkg/nonkube/common/fs_config_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go index 783903feb..f2c5e03af 100644 --- a/pkg/nonkube/common/fs_config_renderer.go +++ b/pkg/nonkube/common/fs_config_renderer.go @@ -483,7 +483,7 @@ func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.Si } func (c *FileSystemConfigurationRenderer) loadCertAsSecretFrom(basePath string, siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) { - certPath := path.Join(basePath, fmt.Sprintf("certificates/%s", purpose), name) + certPath := path.Join(basePath, "certificates", purpose, name) var secret *corev1.Secret certDir, err := os.Open(certPath) if err != nil { From 11de12c9de4582012f2ba315a80733961f148245 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Tue, 8 Oct 2024 17:10:07 -0300 Subject: [PATCH 14/16] Improving provided certificates readme --- cmd/bootstrap/PROVIDED_CERTIFICATES.md | 102 +++++++++++++++++++++---- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md index 9b3fbe2f3..cf5a7cae4 100644 --- a/cmd/bootstrap/PROVIDED_CERTIFICATES.md +++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md @@ -2,16 +2,82 @@ Users can provide their own certificates to be used with Skupper V2 in non kube sites during the bootstrap of a local site, when preparing a site bundle and even while installing -a site bundle at a remote machine. +a site bundle at a remote machine. -## Certificate Authorities (CAs) +## How certificates are used internally -Certificate Authorities (CAs) can be provided at the time a local site is initialized -or a site bundle is being prepared. After a local site is created or a bundle is produced, -CAs won't be used to sign certificates, unless an active namespace is re-initialized. +During the initialization of a local non kube site or when a site bundle is being prepared, +Skupper generates an internal Certificate Authority (CA) named `skupper-site-ca`. -As an example, if you want Skupper to use your own CA certificates to generate and sign server -and client certificates used for site linking, you can simply create the following structure under +If a `RouterAccess` is defined as part of the non kube site definition, Skupper will also +generate a server certificate that is valid for the provided `RouterAccess.spec.bindHost` and +for each entry in the `RouterAccess.spec.subjectAlternativeName` list. The respective certificate +is signed by the CA mentioned earlier (skupper-site-ca). + +A set of static links are also created for the provided `RouterAccess.spec.bindHost` and for each +entry in the `RouterAccess.spec.subjectAlternativeName` list as separate YAML files. + +These static links are composed by a `Link` and a `Secret`, which is basically the client certificate +signed by `skupper-site-ca`, that will be used by other sites to establish Skupper links. + +After a site bundle has been produced, it contains the whole site definition that can be installed +at a remote location. Thus, all certificates and static links are already part of the bundle and no +new certificate is signed at the moment a bundle is installed. + +## Providing your own certificates + +You can provide your own certificates to be used by Skupper for site linking. +Depending on your goals, some certificates should be supplied at certain phases, +for example, at the time a local site is being initialized, a bundle is being prepared +or a bundle is being installed at a remote location. + +To understand it better, let's go through the main use cases and review what is the ideal +phase that a given kind of certificate should be provided. + +### Using a custom CA to sign certificates + +If you want Skupper to generate client and server certificates signed by a custom CA, +you will need to provide the respective certificates during: + +* Local site initialization time +* Site bundle preparation time + +Certificates are signed by Skupper at the time a local namespace is being initialized +or a bundle is being produced. + +After a site bundle has been produced, it already contains the whole definition and no +new certificate is expected to be signed. + +### Using custom server and client certificates + +If you want a new local site to use your custom server and client certificates, you can +provide them at any time, for example: + +* Local site initialization time +* Site bundle preparation time +* Site bundle installation time + +During "Local site initialization time" or "Site bundle preparation time", Skupper will detect +that a server certificate has been provided and will inspect it to determine its subject +alternative names and based on that, a set of static links will be created, allowing those +links to be distributed to target sites accordingly. For the static links to remain valid, +the expected client certificate must also be provided. + +If a server certificate is provided at "Site bundle installation time", Skupper will also try to +determine its subject alternative names using the `openssl` binary (only if available) and it will +use it to generate the static links for the bundle installation. This way, the set of static links +available for an installed bundle will be valid for all expected target hostnames and IP addresses +defined through the server certificate. + +Again, if a server certificate is provided, the respective client certificate is also expected +so that the static links have valid client credentials. + +## Examples + +## Provide your own skupper-site-ca + +If you want Skupper to use your own CA certificates to generate and sign server and client +certificates used for site linking, you can simply create the following structure under the namespace home of your choice, for example: ```shell @@ -23,24 +89,25 @@ ${HOME}/.local/share/skupper/namespaces/default/input/certificates/ └── tls.key ``` -With that if you bootstrap a site to run in the default namespace, the CA certificates above will be -used to generate the server and client certificates for site linking. +With that, if you bootstrap a site to run in the default namespace, the CA certificates above will be +used to sign the server and client certificates for site linking for each provided `RouterAccess`. -## Server and Client certificates (for site linking) +Note that if a CA is provided at the time a site bundle is being installed, it will be detected, +but it won't be used unless the respective namespace is re-initialized. That is because when a bundle +is being installed, it will simply copy certificates provided by the user to be used internally, but +no new certificate will be signed during a site bundle installation. -Server and client certificates can also be provided to help with site linking. +## Server and Client certificates -When a local site is initialized, a bundle is being prepared or installed, Skupper will -inspect the Subject Alternative Names (SANs) from the provided server certificate, and -it will generate a static link for each of the entries, so that they can be distributed -to the appropriate client sites for site linking. +Server and client certificates can be provided whenever your site definition contains at least +one `RouterAccess` (resource). The expected directory names for the server and client certificates, is determined based on the values of `RouterAccess.spec.tlsCredentials` (optional field), or `RouterAccess.name` (default). Supposing the value of `RouterAccess.spec.tlsCredentials` or `RouterAccess.name` (when the tlsCredentials field is omitted) is `my-router-access`, then the following structure, for server and client certificates, -must be provided: +must be provided under the namespace home of your choice, for example: ```shell ${HOME}/.local/share/skupper/namespaces/default/input/certificates/ @@ -78,3 +145,6 @@ $HOME/.local/share/skupper/namespaces/default/runtime/link/link-my-router-access If the respective server certificates are defined at bundle installation time, Skupper will also inspect the subject alternative names of the public server certificate and create the static links for each domain name and ip address found, only if the `openssl` binary is available. + +It is important that the client certificate is also provided, as all static links will be updated +to use the provided client credentials. From 810bee175594eb77564a0acba5427ccda42a7257 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Wed, 9 Oct 2024 11:15:38 -0300 Subject: [PATCH 15/16] Explaining expected directory tree in the file system --- cmd/bootstrap/PROVIDED_CERTIFICATES.md | 31 ++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md index cf5a7cae4..77f2a2625 100644 --- a/cmd/bootstrap/PROVIDED_CERTIFICATES.md +++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md @@ -9,16 +9,36 @@ a site bundle at a remote machine. During the initialization of a local non kube site or when a site bundle is being prepared, Skupper generates an internal Certificate Authority (CA) named `skupper-site-ca`. +The certificates generated by Skupper are stored under the namespace home directory in the +file system, using the following tree: + +```shell +certificates/ +├── ca +├── client +└── server +``` + +As the CA `skupper-site-ca` is generated, it will be stored under the `certificates/ca` path +as a directory named `skupper-site-ca`. Inside a certificate folder, the following files are +expected: `tls.key`, `tls.crt` and `ca.crt` (when dealing with a CA, `ca.crt` and `tls.crt` +have the same content). + If a `RouterAccess` is defined as part of the non kube site definition, Skupper will also generate a server certificate that is valid for the provided `RouterAccess.spec.bindHost` and for each entry in the `RouterAccess.spec.subjectAlternativeName` list. The respective certificate -is signed by the CA mentioned earlier (skupper-site-ca). +is signed by the CA mentioned earlier (skupper-site-ca) and it will be named according to the +provided `spec.tlsCredentials` field, or when it is omitted, the certificate will be named as the +RouterAccess and stored as a directory under `certificates/server`. A set of static links are also created for the provided `RouterAccess.spec.bindHost` and for each entry in the `RouterAccess.spec.subjectAlternativeName` list as separate YAML files. These static links are composed by a `Link` and a `Secret`, which is basically the client certificate -signed by `skupper-site-ca`, that will be used by other sites to establish Skupper links. +signed by `skupper-site-ca`, that will be used by other sites to establish Skupper links. The client +certificate is also named based on the RouterAccess `spec.tlsCredentials` field or, when it is omitted, +the certificate will be named as the RouterAccess, prefixed with `client-` and stored under +`certificates/client`. After a site bundle has been produced, it contains the whole site definition that can be installed at a remote location. Thus, all certificates and static links are already part of the bundle and no @@ -27,6 +47,13 @@ new certificate is signed at the moment a bundle is installed. ## Providing your own certificates You can provide your own certificates to be used by Skupper for site linking. + +The provided certificates must be placed in a similar directory tree, as shown before, +but its path under the namespace home is located at `input/certificates` instead. + +Certificates placed under this path will be used by Skupper primarily and +all the other certificates expected by Skupper, will be generated when not provided. + Depending on your goals, some certificates should be supplied at certain phases, for example, at the time a local site is being initialized, a bundle is being prepared or a bundle is being installed at a remote location. From c6b33640086479b634ce92e073ab8a0c4f1652db Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Thu, 10 Oct 2024 15:21:09 -0300 Subject: [PATCH 16/16] Allows providing a custom issuer for a routeraccess --- pkg/nonkube/api/site_state.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/nonkube/api/site_state.go b/pkg/nonkube/api/site_state.go index 33712fcab..dc78d8193 100644 --- a/pkg/nonkube/api/site_state.go +++ b/pkg/nonkube/api/site_state.go @@ -153,6 +153,12 @@ func (s *SiteState) CreateLinkAccessesCertificates() { if linkAccess.Spec.Issuer != "" { linkAccessCaName = linkAccess.Spec.Issuer } + if linkAccessCaName != caName { + s.Certificates[linkAccessCaName] = s.newCertificate(linkAccessCaName, &v1alpha1.CertificateSpec{ + Subject: linkAccessCaName, + Signing: true, + }) + } certName := name if linkAccess.Spec.TlsCredentials != "" { certName = linkAccess.Spec.TlsCredentials