Check certificates command

[#163597674]
This commit is contained in:
dcorbacho 2019-10-04 15:54:19 +01:00
parent 4f5706c174
commit fe2bd8d95e
3 changed files with 250 additions and 2 deletions

View File

@ -14,7 +14,7 @@
## Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved.
defmodule RabbitMQ.CLI.Core.Listeners do
import Record, only: [defrecord: 2, extract: 2]
import Record, only: [defrecord: 3, extract: 2]
import RabbitCommon.Records
import Rabbitmq.Atom.Coerce
@ -22,7 +22,9 @@ defmodule RabbitMQ.CLI.Core.Listeners do
# API
#
defrecord :hostent, extract(:hostent, from_lib: "kernel/include/inet.hrl")
defrecord :certificate, :Certificate, extract(:Certificate, from_lib: "public_key/include/public_key.hrl")
defrecord :tbscertificate, :TBSCertificate, extract(:TBSCertificate, from_lib: "public_key/include/public_key.hrl")
defrecord :validity, :Validity, extract(:Validity, from_lib: "public_key/include/public_key.hrl")
def listeners_on(listeners, target_node) do
Enum.filter(listeners, fn listener(node: node) ->
@ -106,6 +108,100 @@ defmodule RabbitMQ.CLI.Core.Listeners do
end
end
def listener_expiring_within(listener, seconds) do
listener(node: node, protocol: protocol, ip_address: interface, port: port, opts: opts) = listener
certfile = Keyword.get(opts, :certfile)
cacertfile = Keyword.get(opts, :cacertfile)
now = :calendar.datetime_to_gregorian_seconds(:calendar.universal_time())
expiry_date = now + seconds
certfile_expires_on = expired(cert_validity(read_cert(certfile)), expiry_date)
cacertfile_expires_on = expired(cert_validity(read_cert(cacertfile)), expiry_date)
case {certfile_expires_on, cacertfile_expires_on} do
{[], []} ->
false
_ ->
%{
node: node,
protocol: protocol,
interface: interface,
port: port,
certfile: certfile,
cacertfile: cacertfile,
certfile_expires_on: certfile_expires_on,
cacertfile_expires_on: cacertfile_expires_on
}
end
end
def expired_listener_map(%{node: node, protocol: protocol, interface: interface, port: port, certfile_expires_on: certfile_expires_on, cacertfile_expires_on: cacertfile_expires_on, certfile: certfile, cacertfile: cacertfile}) do
%{
node: node,
protocol: protocol,
interface: :inet.ntoa(interface) |> to_string |> maybe_enquote_interface,
port: port,
purpose: protocol_label(to_atom(protocol)),
certfile: certfile |> to_string,
cacertfile: cacertfile |> to_string,
certfile_expires_on: expires_on_list(certfile_expires_on),
cacertfile_expires_on: expires_on_list(cacertfile_expires_on)
}
end
def expires_on_list({:error, _} = error) do
[error]
end
def expires_on_list(expires) do
Enum.map(expires, &expires_on/1)
end
def expires_on({:error, _} = error) do
error
end
def expires_on(seconds) do
{:ok, naive} = NaiveDateTime.from_erl(:calendar.gregorian_seconds_to_datetime(seconds))
NaiveDateTime.to_string(naive)
end
def expired(nil, _) do
[]
end
def expired({:error, _} = error, _) do
error
end
def expired(expires, expiry_date) do
Enum.filter(expires, fn ({:error, _} = e) -> true
(seconds) -> seconds < expiry_date end)
end
def cert_validity(nil) do
nil
end
def cert_validity(cert) do
dsa_entries = :public_key.pem_decode(cert)
case dsa_entries do
[] ->
{:error, "The certificate file provided does not contain any PEM entry."}
_ ->
now = :calendar.datetime_to_gregorian_seconds(:calendar.universal_time())
Enum.map(dsa_entries, fn ({:Certificate, _, _} = dsa_entry) ->
certificate(tbsCertificate: tbs_certificate) = :public_key.pem_entry_decode(dsa_entry)
tbscertificate(validity: validity) = tbs_certificate
validity(notAfter: not_after, notBefore: not_before) = validity
start = :pubkey_cert.time_str_2_gregorian_sec(not_before)
case start > now do
true ->
{:ok, naive} = NaiveDateTime.from_erl(:calendar.gregorian_seconds_to_datetime(start))
startdate = NaiveDateTime.to_string(naive)
{:error, "Certificate is not yet valid. It starts on #{startdate}"}
false ->
:pubkey_cert.time_str_2_gregorian_sec(not_after)
end
({type, _, _}) ->
{:error, "The certificate file provided contains a #{type} entry."}
end)
end
end
def listener_rows(listeners) do
for listener(node: node, protocol: protocol, ip_address: interface, port: port) <- listeners do
# Listener options are left out intentionally, see above

View File

@ -0,0 +1,95 @@
## The contents of this file are subject to the Mozilla Public License
## Version 1.1 (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License
## at https://www.mozilla.org/MPL/
##
## Software distributed under the License is distributed on an "AS IS"
## basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
## the License for the specific language governing rights and
## limitations under the License.
##
## The Original Code is RabbitMQ.
##
## The Initial Developer of the Original Code is GoPivotal, Inc.
## Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved.
defmodule RabbitMQ.CLI.Diagnostics.Commands.CheckCertificateExpirationCommand do
alias RabbitMQ.CLI.Core.DocGuide
alias RabbitMQ.CLI.TimeUnit, as: TU
@behaviour RabbitMQ.CLI.CommandBehaviour
import RabbitMQ.CLI.Core.Listeners
def switches(), do: [unit: :string, within: :integer]
def merge_defaults(args, opts) do
{args, Map.merge(%{unit: "days", within: 1}, opts)}
end
def validate(args, _) when length(args) > 0 do
{:validation_failure, :too_many_args}
end
def validate(_, %{unit: unit}) do
case TU.known_unit?(unit) do
true ->
:ok
false ->
{:validation_failure, "unit '#{unit}' is not supported. Please use one of: days, weeks, months, years"}
end
end
def validate(_, _), do: :ok
def run([], %{node: node_name, unit: unit, within: within, timeout: timeout}) do
case :rabbit_misc.rpc_call(node_name, :rabbit_networking, :active_listeners, [], timeout) do
{:error, _} = err ->
err
{:error, _, _} = err ->
err
{:badrpc, _} = err ->
err
xs when is_list(xs) ->
listeners = listeners_on(xs, node_name)
seconds = TU.convert(within, unit)
Enum.reduce(listeners, [], fn (listener, acc) -> case listener_expiring_within(listener, seconds) do
false -> acc
expiring -> [expiring | acc]
end
end)
end
end
def output([], %{formatter: "json"}) do
{:ok, %{"result" => "ok"}}
end
def output([], %{unit: unit, within: within}) do
{:ok, "No certificates are expiring within #{within} #{unit}."}
end
def output(listeners, %{formatter: "json"}) do
{:error, :check_failed, %{"result" => "error", "expired" => Enum.map(listeners, &expired_listener_map/1)}}
end
def output(listeners, %{}) do
{:error, :check_failed, Enum.map(listeners, &expired_listener_map/1)}
end
def usage, do: "check_certificate_expiration"
def usage_doc_guides() do
[
DocGuide.configuration(),
DocGuide.tls()
]
end
def help_section(), do: :observability_and_health_checks
def description(), do: "Checks the expiration date on the certificates for every listener configured to use TLS"
def banner(_, %{node: node_name}), do: "Expired certificates of node #{node_name} ..."
end

View File

@ -0,0 +1,57 @@
## The contents of this file are subject to the Mozilla Public License
## Version 1.1 (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License
## at https://www.mozilla.org/MPL/
##
## Software distributed under the License is distributed on an "AS IS"
## basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
## the License for the specific language governing rights and
## limitations under the License.
##
## The Original Code is RabbitMQ.
##
## The Initial Developer of the Original Code is GoPivotal, Inc.
## Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved.
defmodule RabbitMQ.CLI.TimeUnit do
require MapSet
@days_seconds 86400
@weeks_seconds @days_seconds * 7
@months_seconds @days_seconds * (365 / 12)
@years_seconds @days_seconds * 365
def known_units() do
MapSet.new([
"days",
"weeks",
"months",
"years"
])
end
def convert(time, unit) do
do_convert(time, String.downcase(unit))
end
def known_unit?(val) do
MapSet.member?(known_units(), String.downcase(val))
end
defp do_convert(time, "days") do
time * @days_seconds
end
defp do_convert(time, "weeks") do
time * @weeks_seconds
end
defp do_convert(time, "months") do
time * @months_seconds
end
defp do_convert(time, "years") do
time * @years_seconds
end
end