diff --git a/ocaml/idl/datamodel_common.ml b/ocaml/idl/datamodel_common.ml index 0e8625b613f..93e6dd522f9 100644 --- a/ocaml/idl/datamodel_common.ml +++ b/ocaml/idl/datamodel_common.ml @@ -8,7 +8,7 @@ open Datamodel_roles When introducing a new release, bump the schema minor version to the next hundred to leave a gap for potential hotfixes needing to increment the schema version.*) let schema_major_vsn = 5 -let schema_minor_vsn = 703 +let schema_minor_vsn = 704 (* Historical schema versions just in case this is useful later *) let rio_schema_major_vsn = 5 diff --git a/ocaml/idl/datamodel_host.ml b/ocaml/idl/datamodel_host.ml index e28ad86ec26..48db442624d 100644 --- a/ocaml/idl/datamodel_host.ml +++ b/ocaml/idl/datamodel_host.ml @@ -1029,6 +1029,14 @@ let host_query_ha = call ~flags:[`Session] ~allowed_roles:_R_READ_ONLY () + let refresh_server_certificate = call + ~lifecycle:[Published, rel_next, ""] + ~name:"refresh_server_certificate" + ~doc:"Replace the internal self-signed host certficate with a new one." + ~params:[Ref _host, "host", "The host"] + ~allowed_roles:_R_POOL_ADMIN + () + let display = Enum ("host_display", [ "enabled", "This host is outputting its console to a physical display device"; @@ -1544,6 +1552,7 @@ let host_query_ha = call ~flags:[`Session] crl_list; certificate_sync; get_server_certificate; + refresh_server_certificate; install_server_certificate; emergency_reset_server_certificate; reset_server_certificate; diff --git a/ocaml/idl/datamodel_pool.ml b/ocaml/idl/datamodel_pool.ml index 8e6356e2c93..5c909fb188d 100644 --- a/ocaml/idl/datamodel_pool.ml +++ b/ocaml/idl/datamodel_pool.ml @@ -13,6 +13,7 @@ open Datamodel_types "sync_updates", "Indicates this pool is in the process of syncing updates"; "get_updates", "Indicates this pool is in the process of getting updates"; "apply_updates", "Indicates this pool is in the process of applying updates"; + "cert_refresh", "A certificate refresh and distribution is in progress"; ]) let enable_ha = call diff --git a/ocaml/idl/schematest.ml b/ocaml/idl/schematest.ml index 510ea65fd5b..33ac59b2217 100644 --- a/ocaml/idl/schematest.ml +++ b/ocaml/idl/schematest.ml @@ -1,7 +1,7 @@ let hash x = Digest.string x |> Digest.to_hex (* BEWARE: if this changes, check that schema has been bumped accordingly *) -let last_known_schema_hash = "633a99b46dda090677598aad3a830f76" +let last_known_schema_hash = "632046e456380e732bb9faadde0ecb9e" let current_schema_hash : string = let open Datamodel_types in diff --git a/ocaml/xapi-cli-server/cli_frontend.ml b/ocaml/xapi-cli-server/cli_frontend.ml index 2609d90e312..395843a63a1 100644 --- a/ocaml/xapi-cli-server/cli_frontend.ml +++ b/ocaml/xapi-cli-server/cli_frontend.ml @@ -3071,6 +3071,15 @@ let rec cmdtable_data : (string * cmd_spec) list = ; flags= [Host_selectors] } ) + ; ( "host-refresh-server-certificate" + , { + reqd= ["host"] + ; optn= [] + ; help= "Refresh internal server certificate of host" + ; implementation= No_fd Cli_operations.host_refresh_server_certificate + ; flags= [Host_selectors] + } + ) ; ( "host-server-certificate-install" , { reqd= ["certificate"; "private-key"] diff --git a/ocaml/xapi-cli-server/cli_operations.ml b/ocaml/xapi-cli-server/cli_operations.ml index 863f522d354..f81b1cb27fb 100644 --- a/ocaml/xapi-cli-server/cli_operations.ml +++ b/ocaml/xapi-cli-server/cli_operations.ml @@ -3504,6 +3504,16 @@ let host_get_server_certificate printer rpc session_id params = params [] ) +let host_refresh_server_certificate printer rpc session_id params = + ignore + (do_host_op rpc session_id ~multiple:false + (fun _ host -> + let host = host.getref () in + Client.Host.refresh_server_certificate rpc session_id host + ) + params [] + ) + let host_install_server_certificate fd printer rpc session_id params = let certificate = List.assoc "certificate" params |> get_file_or_fail fd "certificate" diff --git a/ocaml/xapi-cli-server/record_util.ml b/ocaml/xapi-cli-server/record_util.ml index dab03fec60f..6b97da971f4 100644 --- a/ocaml/xapi-cli-server/record_util.ml +++ b/ocaml/xapi-cli-server/record_util.ml @@ -164,6 +164,8 @@ let pool_operation_to_string = function "get_updates" | `apply_updates -> "apply_updates" + | `cert_refresh -> + "cert_refresh" let host_operation_to_string = function | `provision -> diff --git a/ocaml/xapi-consts/api_errors.ml b/ocaml/xapi-consts/api_errors.ml index acc9ad8a32e..c5bd4083c32 100644 --- a/ocaml/xapi-consts/api_errors.ml +++ b/ocaml/xapi-consts/api_errors.ml @@ -1221,6 +1221,8 @@ let pool_secret_rotation_pending = "POOL_SECRET_ROTATION_PENDING" let tls_verification_enable_in_progress = "TLS_VERIFICATION_ENABLE_IN_PROGRESS" +let cert_refresh_in_progress = "CERT_REFRESH_IN_PROGRESS" + let configure_repositories_in_progress = "CONFIGURE_REPOSITORIES_IN_PROGRESS" let invalid_base_url = "INVALID_BASE_URL" diff --git a/ocaml/xapi/cert_distrib.ml b/ocaml/xapi/cert_distrib.ml index 955b2b50d22..d8771c051eb 100644 --- a/ocaml/xapi/cert_distrib.ml +++ b/ocaml/xapi/cert_distrib.ml @@ -439,3 +439,17 @@ let import_joining_pool_ca_certificates ~__context ~ca_certs = Worker.local_write_cert_fs ~__context ApplianceCertificate Merge appliance_certs ; Worker.local_regen_bundle ~__context + +let distribute_new_host_cert ~__context ~host ~content = + let hosts = Db.Host.get_all ~__context in + let uuid = Db.Host.get_uuid ~__context ~self:host in + let file = + WireProtocol.{filename= Printf.sprintf "%s.new.pem" uuid; content} + in + let job rpc session_id host = + Worker.remote_write_certs_fs HostPoolCertificate Merge [file] host rpc + session_id + in + Helpers.call_api_functions ~__context @@ fun rpc session_id -> + List.iter (fun host -> job rpc session_id host) hosts ; + List.iter (fun host -> Worker.remote_regen_bundle host rpc session_id) hosts diff --git a/ocaml/xapi/cert_distrib.mli b/ocaml/xapi/cert_distrib.mli index 8c1f91e110f..fa78acde9fb 100644 --- a/ocaml/xapi/cert_distrib.mli +++ b/ocaml/xapi/cert_distrib.mli @@ -50,3 +50,7 @@ val import_joining_pool_ca_certificates : This parameter must be the result of [exchange_ca_certificates_with_joiner]. This function was designed as part of pool join and is unlikely to be useful elsewhere. *) + +val distribute_new_host_cert : + __context:Context.t -> host:[`host] API.Ref.t -> content:string -> unit +(** distribute a new (additional) certificate for [host] in the pool *) diff --git a/ocaml/xapi/cert_refresh.ml b/ocaml/xapi/cert_refresh.ml new file mode 100644 index 00000000000..7f2a85f234f --- /dev/null +++ b/ocaml/xapi/cert_refresh.ml @@ -0,0 +1,110 @@ +(* + * Copyright (C) Citrix Systems Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +module D = Debug.Make (struct let name = "cert_refresh" end) + +open D + +(* given a [filename], replace its extension with [ext]. Does not change +the file system *) +let replace_extension filename ~ext = + let base = + match Filename.extension filename with + | "" -> + filename + | _ -> + Filename.remove_extension filename + in + Printf.sprintf "%s.%s" base ext + +(* Path to host server certificate (in PEM format) *) +let cert_path = function + | `host -> + !Xapi_globs.server_cert_path + | `host_internal -> + !Xapi_globs.server_cert_internal_path + +(* Paths to certificates that we are about to use *) +let new_cert_path type' = replace_extension (cert_path type') ~ext:"new" + +let backup_cert_path type' = replace_extension (cert_path type') ~ext:"bak" + +(* Create a new host cert in the file system and return its contents +also as a data structure *) +let new_host_cert ~dbg ~path : X509.Certificate.t = + let name, ip = + match Networking_info.get_management_ip_addr ~dbg with + | None -> + let msg = Printf.sprintf "%s: failed to get management IP" __LOC__ in + D.error "%s" msg ; + raise Api_errors.(Server_error (internal_error, [msg])) + | Some ip -> + ip + in + let dns_names = Networking_info.dns_names () in + let ips = [ip] in + Gencertlib.Selfcert.host ~name ~dns_names ~ips path + +(* On this host and for this host, create a new server certificate and +distribute it in the pool *) +let host ~__context ~type' = + let host = Helpers.get_localhost ~__context in + let dbg = Context.string_of_task __context in + let pem = cert_path type' in + let path = new_cert_path type' in + let cert = new_host_cert ~dbg ~path in + let bak = backup_cert_path type' in + let content = X509.Certificate.encode_pem cert |> Cstruct.to_string in + (* distribute public part of new cert in pool *) + Cert_distrib.distribute_new_host_cert ~__context ~host ~content ; + (* replace certs in file system on host *) + info "renaming cert %s to %s" path pem ; + Sys.rename pem bak ; + Sys.rename path pem ; + (* remove old from database, add new *) + Certificates.Db_util.get_host_certs ~__context ~type' ~host + |> List.iter (Certificates.Db_util.remove_cert_by_ref ~__context) ; + let ref = + match type' with + | `host -> + Certificates.Db_util.add_cert ~__context ~type':(`host host) cert + | `host_internal -> + Certificates.Db_util.add_cert ~__context ~type':(`host_internal host) + cert + in + (* start using new cert *) + Helpers.Stunnel.reload () ; ref + +(* The stunnel clients trust the old and the new [host] server cert. On +the local host, rename the old cert and re-create the cert bundle +without it *) +let remove_stale_cert ~__context ~host ~type' = + let uuid = Db.Host.get_uuid ~__context ~self:host in + let directory = + match type' with + | `host -> + !Xapi_globs.trusted_certs_dir + | `host_internal -> + !Xapi_globs.trusted_pool_certs_dir + in + let next = Filename.concat directory (Printf.sprintf "%s.new.pem" uuid) in + let pem = Filename.concat directory (Printf.sprintf "%s.pem" uuid) in + let bak = Filename.concat directory (Printf.sprintf "%s.bak" uuid) in + if Sys.file_exists next && Sys.file_exists pem then ( + info "cleanup - renaming %s to %s" next pem ; + Sys.rename pem bak ; + Sys.rename next pem ; + Certificates.update_ca_bundle () + ) else + info "cleanup - no new cert %s found - skipping" next diff --git a/ocaml/xapi/cert_refresh.mli b/ocaml/xapi/cert_refresh.mli new file mode 100644 index 00000000000..425e2c95e09 --- /dev/null +++ b/ocaml/xapi/cert_refresh.mli @@ -0,0 +1,31 @@ +(* + * Copyright (C) Citrix Systems Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +(** For this host, generate a new host certificate and distribute its +public parts. When this function returns, pool members still would +accept the old certificate and hence need to perform some clean-up +actions *) + +val host : + __context:Context.t -> type':[< `host | `host_internal] -> API.ref_Certificate + +(** On this host, remove stale certs for [host] after [host]'s +certificates were refreshed. This needs to be executed on every +host of the pool *) + +val remove_stale_cert : + __context:Context.t + -> host:API.ref_host + -> type':[< `host | `host_internal] + -> unit diff --git a/ocaml/xapi/certificates.ml b/ocaml/xapi/certificates.ml index a6bfaae5d13..8474cd54d20 100644 --- a/ocaml/xapi/certificates.ml +++ b/ocaml/xapi/certificates.ml @@ -438,11 +438,10 @@ let hostnames_of_pem_cert pem = >>| X509.Certificate.hostnames let install_server_certificate ?(pem_chain = None) ~pem_leaf ~pkcs8_private_key - = - let server_cert_path = !Xapi_globs.server_cert_path in + ~path = let installation = Gencertlib.Lib.install_server_certificate ~pem_chain ~pem_leaf - ~pkcs8_private_key ~server_cert_path + ~pkcs8_private_key ~server_cert_path:path in match installation with | Ok cert -> diff --git a/ocaml/xapi/helpers.ml b/ocaml/xapi/helpers.ml index 7b2a75e2552..33ad4e48f41 100644 --- a/ocaml/xapi/helpers.ml +++ b/ocaml/xapi/helpers.ml @@ -1816,6 +1816,9 @@ let try_internal_async ~__context (marshaller : Rpc.t -> 'b) module Stunnel : sig val restart : __context:Context.t -> accept:string -> unit (** restart stunnel, possibly changing the config file *) + + val reload : unit -> unit + (** reload (potentially updated) configuration *) end = struct let cert = !Xapi_globs.server_cert_path @@ -1902,6 +1905,8 @@ end = struct let systemctl_ cmd = systemctl cmd |> ignore + let reload () = systemctl_ "reload-or-restart" + let is_enabled () = let is_enabled_stdout = try systemctl "is-enabled" diff --git a/ocaml/xapi/message_forwarding.ml b/ocaml/xapi/message_forwarding.ml index 47cd35153fa..ae3cf3ea0d0 100644 --- a/ocaml/xapi/message_forwarding.ml +++ b/ocaml/xapi/message_forwarding.ml @@ -3582,6 +3582,29 @@ functor Client.Host.get_server_certificate rpc session_id host ) + let refresh_server_certificate ~__context ~host = + info "Host.refresh_server_certificate: host = '%s'" + (host_uuid ~__context host) ; + let pool = Helpers.get_pool ~__context in + let local_fn = Local.Host.refresh_server_certificate ~host in + let other = + Db.Host.get_all ~__context |> List.filter (fun h -> h <> host) + in + Xapi_pool_helpers.with_pool_operation ~__context + ~doc:"Host.refresh_server_certificate" ~self:pool ~op:`cert_refresh + @@ fun () -> + (* let host refresh its certificates first *) + do_op_on ~local_fn ~__context ~host (fun session_id rpc -> + Client.Host.refresh_server_certificate rpc session_id host + ) ; + (* update all other hosts in the pool *) + other + |> List.iter (fun h -> + do_op_on ~local_fn ~__context ~host:h (fun session_id rpc -> + Client.Host.refresh_server_certificate rpc session_id host + ) + ) + let _success ~__context () = let task = Context.get_task_id __context in let progress = Db.Task.get_progress ~__context ~self:task in diff --git a/ocaml/xapi/xapi_host.ml b/ocaml/xapi/xapi_host.ml index 6fd502cc9fa..a8bf90fc1ee 100644 --- a/ocaml/xapi/xapi_host.ml +++ b/ocaml/xapi/xapi_host.ml @@ -1465,7 +1465,11 @@ let replace_host_certificate ~__context ~type' ~host let old_certs = Db_util.get_host_certs ~__context ~type' ~host in let new_cert = write_cert_fs () in let (_ : API.ref_Certificate) = - Db_util.add_cert ~__context ~type':(`host host) new_cert + match type' with + | `host -> + Db_util.add_cert ~__context ~type':(`host host) new_cert + | `host_internal -> + Db_util.add_cert ~__context ~type':(`host_internal host) new_cert in List.iter (Db_util.remove_cert_by_ref ~__context) old_certs ; let task = Context.get_task_id __context in @@ -1476,17 +1480,17 @@ let install_server_certificate ~__context ~host ~certificate ~private_key ~certificate_chain = if Db.Pool.get_ha_enabled ~__context ~self:(Helpers.get_pool ~__context) then raise Api_errors.(Server_error (ha_is_enabled, [])) ; + let path = !Xapi_globs.server_cert_path in let write_cert_fs () = let pem_chain = match certificate_chain with "" -> None | pem_chain -> Some pem_chain in Certificates.install_server_certificate ~pem_leaf:certificate - ~pkcs8_private_key:private_key ~pem_chain + ~pkcs8_private_key:private_key ~pem_chain ~path in replace_host_certificate ~__context ~type':`host ~host write_cert_fs -let _new_host_cert ~dbg : X509.Certificate.t = - let xapi_ssl_pem = !Xapi_globs.server_cert_path in +let _new_host_cert ~dbg ~path : X509.Certificate.t = let name, ip = match Networking_info.get_management_ip_addr ~dbg with | None -> @@ -1498,19 +1502,36 @@ let _new_host_cert ~dbg : X509.Certificate.t = in let dns_names = Networking_info.dns_names () in let ips = [ip] in - Gencertlib.Selfcert.host ~name ~dns_names ~ips xapi_ssl_pem + Gencertlib.Selfcert.host ~name ~dns_names ~ips path let reset_server_certificate ~__context ~host = let dbg = Context.string_of_task __context in - let write_cert_fs () = _new_host_cert ~dbg in + let path = !Xapi_globs.server_cert_path in + let write_cert_fs () = _new_host_cert ~dbg ~path in replace_host_certificate ~__context ~type':`host ~host write_cert_fs let emergency_reset_server_certificate ~(__context : 'a) = + let path = !Xapi_globs.server_cert_path in let (_ : X509.Certificate.t) = - _new_host_cert ~dbg:"emergency_reset_certificate" + _new_host_cert ~dbg:"emergency_reset_certificate" ~path in () +let refresh_server_certificate ~__context ~host = + (* we need to do different things depending on whether we + refresh the certificates on this host or whether they were + refreshed on another host in the pool *) + let localhost = Helpers.get_localhost ~__context in + ( match host with + | host when host = localhost -> + debug "Host.refresh_server_certificates - refresh this host (1/2)" ; + ignore @@ Cert_refresh.host ~__context ~type':`host_internal + | host -> + debug "Host.refresh_server_certificates - host %s was refrehsed" + (Ref.string_of host) + ) ; + Cert_refresh.remove_stale_cert ~__context ~host ~type':`host_internal + (* CA-24856: detect non-homogeneous external-authentication config in pool *) let detect_nonhomogeneous_external_auth_in_host ~__context ~host = Helpers.call_api_functions ~__context (fun rpc session_id -> diff --git a/ocaml/xapi/xapi_host.mli b/ocaml/xapi/xapi_host.mli index a6ab20fbb25..635f6f0a66e 100644 --- a/ocaml/xapi/xapi_host.mli +++ b/ocaml/xapi/xapi_host.mli @@ -291,6 +291,9 @@ val certificate_sync : __context:'a -> host:'b -> unit val get_server_certificate : __context:'a -> host:'b -> string +val refresh_server_certificate : + __context:Context.t -> host:[`host] Ref.t -> unit + val install_server_certificate : __context:Context.t -> host:[`host] Ref.t diff --git a/ocaml/xapi/xapi_pool_helpers.ml b/ocaml/xapi/xapi_pool_helpers.ml index bd987579372..a2c8770745d 100644 --- a/ocaml/xapi/xapi_pool_helpers.ml +++ b/ocaml/xapi/xapi_pool_helpers.ml @@ -35,6 +35,7 @@ let all_operations = ; `sync_updates ; `get_updates ; `apply_updates + ; `cert_refresh ] (** Returns a table of operations -> API error options (None if the operation would be ok) *) @@ -70,6 +71,7 @@ let valid_operations ~__context record _ref' = ; (`sync_updates, Api_errors.sync_updates_in_progress, []) ; (`get_updates, Api_errors.get_updates_in_progress, []) ; (`apply_updates, Api_errors.apply_updates_in_progress, []) + ; (`cert_refresh, Api_errors.cert_refresh_in_progress, []) ] in List.iter