Software /
code /
prosody
File
util/prosodyctl/check.lua @ 11776:1132a1f1ca5a
util.prosodyctl.check: Check for server-to-server Direct TLS records
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Thu, 09 Sep 2021 22:09:41 +0200 |
parent | 11655:bbf50525faa5 |
child | 11777:08de090e05e9 |
line wrap: on
line source
local configmanager = require "core.configmanager"; local show_usage = require "util.prosodyctl".show_usage; local show_warning = require "util.prosodyctl".show_warning; local dependencies = require "util.dependencies"; local socket = require "socket"; local jid_split = require "util.jid".prepped_split; local modulemanager = require "core.modulemanager"; local function check(arg) if arg[1] == "--help" then show_usage([[check]], [[Perform basic checks on your Prosody installation]]); return 1; end local what = table.remove(arg, 1); local set = require "util.set"; local it = require "util.iterators"; local ok = true; local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs") then show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs' or 'disabled'.", what); return 1; end if not what or what == "disabled" then local disabled_hosts_set = set.new(); for host, host_options in it.filter("*", pairs(configmanager.getconfig())) do if host_options.enabled == false then disabled_hosts_set:add(host); end end if not disabled_hosts_set:empty() then local msg = "Checks will be skipped for these disabled hosts: %s"; if what then msg = "These hosts are disabled: %s"; end show_warning(msg, tostring(disabled_hosts_set)); if what then return 0; end print"" end end if not what or what == "config" then print("Checking config..."); local deprecated = set.new({ "anonymous_login", "bosh_ports", "cross_domain_bosh", "cross_domain_websocket", "daemonize", "disallow_s2s", "legacy_ssl_interfaces", "legacy_ssl_port", "legacy_ssl_ports", "legacy_ssl_ssl", "no_daemonize", "require_encryption", "vcard_compatibility", }); local known_global_options = set.new({ "access_control_allow_credentials", "access_control_allow_headers", "access_control_allow_methods", "access_control_max_age", "admin_socket", "body_size_limit", "bosh_max_inactivity", "bosh_max_polling", "bosh_max_wait", "buffer_size_limit", "c2s_close_timeout", "c2s_stanza_size_limit", "c2s_tcp_keepalives", "c2s_timeout", "component_stanza_size_limit", "component_tcp_keepalives", "consider_bosh_secure", "consider_websocket_secure", "console_banner", "console_prettyprint_settings", "cross_domain_bosh", "cross_domain_websocket", "daemonize", "gc", "http_default_host", "http_errors_always_show", "http_errors_default_message", "http_errors_detailed", "http_errors_messages", "installer_plugin_path", "limits", "limits_resolution", "log", "multiplex_buffer_size", "network_backend", "network_default_read_size", "network_settings", "pidfile", "plugin_paths", "plugin_server", "prosodyctl_timeout", "prosody_group", "prosody_user", "run_as_root", "s2s_close_timeout", "s2s_insecure_domains", "s2s_require_encryption", "s2s_secure_auth", "s2s_secure_domains", "s2s_stanza_size_limit", "s2s_tcp_keepalives", "s2s_timeout", "statistics", "statistics_config", "statistics_interval", "tcp_keepalives", "trusted_proxies", "umask", "use_dane", "use_ipv4", "use_ipv6", "use_libevent", "websocket_frame_buffer_limit", "websocket_frame_fragment_limit", "websocket_get_response_body", "websocket_get_response_text", }); local config = configmanager.getconfig(); -- Check that we have any global options (caused by putting a host at the top) if it.count(it.filter("log", pairs(config["*"]))) == 0 then ok = false; print(""); print(" No global options defined. Perhaps you have put a host definition at the top") print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview"); end if it.count(enabled_hosts()) == 0 then ok = false; print(""); if it.count(it.filter("*", pairs(config))) == 0 then print(" No hosts are defined, please add at least one VirtualHost section") elseif config["*"]["enabled"] == false then print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section") else print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section") end end if not config["*"].modules_enabled then print(" No global modules_enabled is set?"); local suggested_global_modules; for host, options in enabled_hosts() do --luacheck: ignore 213/host if not options.component_module and options.modules_enabled then suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled)); end end if suggested_global_modules and not suggested_global_modules:empty() then print(" Consider moving these modules into modules_enabled in the global section:") print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end)); end print(); end do -- Check for modules enabled both normally and as components local modules = set.new(config["*"]["modules_enabled"]); for host, options in enabled_hosts() do local component_module = options.component_module; if component_module and modules:contains(component_module) then print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module)); print(" This means the service is enabled on all VirtualHosts as well as the Component."); print(" Are you sure this what you want? It may cause unexpected behaviour."); end end end -- Check for global options under hosts local global_options = set.new(it.to_array(it.keys(config["*"]))); local deprecated_global_options = set.intersection(global_options, deprecated); if not deprecated_global_options:empty() then print(""); print(" You have some deprecated options in the global section:"); print(" "..tostring(deprecated_global_options)) ok = false; end for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do local host_options = set.new(it.to_array(it.keys(options))); local misplaced_options = set.intersection(host_options, known_global_options); for name in pairs(options) do if name:match("^interfaces?") or name:match("_ports?$") or name:match("_interfaces?$") or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then misplaced_options:add(name); end end if not misplaced_options:empty() then ok = false; print(""); local n = it.count(misplaced_options); print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be"); print(" in the global section of the config file, above any VirtualHost or Component definitions,") print(" see https://prosody.im/doc/configure#overview for more information.") print(""); print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", ")); end end for host, options in enabled_hosts() do local host_options = set.new(it.to_array(it.keys(options))); local subdomain = host:match("^[^.]+"); if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp" or subdomain == "chat" or subdomain == "im") then print(""); print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to"); print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host.."."); print(" For more information see: https://prosody.im/doc/dns"); end end local all_modules = set.new(config["*"].modules_enabled); local all_options = set.new(it.to_array(it.keys(config["*"]))); for host in enabled_hosts() do all_options:include(set.new(it.to_array(it.keys(config[host])))); all_modules:include(set.new(config[host].modules_enabled)); end for mod in all_modules do if mod:match("^mod_") then print(""); print(" Modules in modules_enabled should not have the 'mod_' prefix included."); print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'."); elseif mod:match("^auth_") then print(""); print(" Authentication modules should not be added to modules_enabled,"); print(" but be specified in the 'authentication' option."); print(" Remove '"..mod.."' from modules_enabled and instead add"); print(" authentication = '"..mod:match("^auth_(.*)").."'"); print(" For more information see https://prosody.im/doc/authentication"); elseif mod:match("^storage_") then print(""); print(" storage modules should not be added to modules_enabled,"); print(" but be specified in the 'storage' option."); print(" Remove '"..mod.."' from modules_enabled and instead add"); print(" storage = '"..mod:match("^storage_(.*)").."'"); print(" For more information see https://prosody.im/doc/storage"); end end if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then print(""); print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict"); print(" with each other. Remove one."); end if all_modules:contains("pep") and all_modules:contains("pep_simple") then print(""); print(" Both mod_pep_simple and mod_pep are enabled but they conflict"); print(" with each other. Remove one."); end for host, host_config in pairs(config) do --luacheck: ignore 213/host if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then print(""); print(" The 'default_storage' option is not needed if 'storage' is set to a string."); break; end end local require_encryption = set.intersection(all_options, set.new({ "require_encryption", "c2s_require_encryption", "s2s_require_encryption" })):empty(); local ssl = dependencies.softreq"ssl"; if not ssl then if not require_encryption then print(""); print(" You require encryption but LuaSec is not available."); print(" Connections will fail."); ok = false; end elseif not ssl.loadcertificate then if all_options:contains("s2s_secure_auth") then print(""); print(" You have set s2s_secure_auth but your version of LuaSec does "); print(" not support certificate validation, so all s2s connections will"); print(" fail."); ok = false; elseif all_options:contains("s2s_secure_domains") then local secure_domains = set.new(); for host in enabled_hosts() do if config[host].s2s_secure_auth == true then secure_domains:add("*"); else secure_domains:include(set.new(config[host].s2s_secure_domains)); end end if not secure_domains:empty() then print(""); print(" You have set s2s_secure_domains but your version of LuaSec does "); print(" not support certificate validation, so s2s connections to/from "); print(" these domains will fail."); ok = false; end end elseif require_encryption and not all_modules:contains("tls") then print(""); print(" You require encryption but mod_tls is not enabled."); print(" Connections will fail."); ok = false; end print("Done.\n"); end if not what or what == "dns" then local dns = require "net.dns"; pcall(function () local unbound = require"net.unbound"; local unbound_config = configmanager.get("*", "unbound") or {}; unbound_config.hoststxt = false; -- don't look at /etc/hosts configmanager.set("*", "unbound", unbound_config); unbound.purge(); -- ensure the above config is used dns = unbound.dns; end) local idna = require "util.encodings".idna; local ip = require "util.ip"; local c2s_ports = set.new(configmanager.get("*", "c2s_ports") or {5222}); local s2s_ports = set.new(configmanager.get("*", "s2s_ports") or {5269}); local c2s_tls_ports = set.new(configmanager.get("*", "direct_tls_ports") or {}); local s2s_tls_ports = set.new(configmanager.get("*", "s2s_direct_tls_ports") or {}); local c2s_srv_required, s2s_srv_required, c2s_tls_srv_required, s2s_tls_srv_required; if not c2s_ports:contains(5222) then c2s_srv_required = true; end if not s2s_ports:contains(5269) then s2s_srv_required = true; end if not c2s_tls_ports:empty() then c2s_tls_srv_required = true; end if not s2s_tls_ports:empty() then s2s_tls_srv_required = true; end local problem_hosts = set.new(); local external_addresses, internal_addresses = set.new(), set.new(); local fqdn = socket.dns.tohostname(socket.dns.gethostname()); if fqdn then do local res = dns.lookup(idna.to_ascii(fqdn), "A"); if res then for _, record in ipairs(res) do external_addresses:add(record.a); end end end do local res = dns.lookup(idna.to_ascii(fqdn), "AAAA"); if res then for _, record in ipairs(res) do external_addresses:add(record.aaaa); end end end end local local_addresses = require"util.net".local_addresses() or {}; for addr in it.values(local_addresses) do if not ip.new_ip(addr).private then external_addresses:add(addr); else internal_addresses:add(addr); end end if external_addresses:empty() then print(""); print(" Failed to determine the external addresses of this server. Checks may be inaccurate."); c2s_srv_required, s2s_srv_required = true, true; end local v6_supported = not not socket.tcp6; local function trim_dns_name(n) return (n:gsub("%.$", "")); end for jid, host_options in enabled_hosts() do local all_targets_ok, some_targets_ok = true, false; local node, host = jid_split(jid); local modules, component_module = modulemanager.get_modules_for_host(host); if component_module then modules:add(component_module); end local is_component = not not host_options.component_module; print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."..."); if node then print("Only the domain part ("..host..") is used in DNS.") end local target_hosts = set.new(); if modules:contains("c2s") then local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO is this an error if mod_c2s is enabled? print(" 'xmpp-client' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not c2s_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown client port: "..record.srv.port); end end else if c2s_srv_required then print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; else target_hosts:add(host); end end end if modules:contains("c2s") and c2s_tls_srv_required then local res = dns.lookup("_xmpps-client._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO is this an error if mod_c2s is enabled? print(" 'xmpps-client' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not c2s_tls_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown Direct TLS client port: "..record.srv.port); end end else print(" No _xmpps-client SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; end end if modules:contains("s2s") then local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO Is this an error if mod_s2s is enabled? print(" 'xmpp-server' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not s2s_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown server port: "..record.srv.port); end end else if s2s_srv_required then print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; else target_hosts:add(host); end end end if modules:contains("s2s") and s2s_tls_srv_required then local res = dns.lookup("_xmpps-server._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO is this an error if mod_s2s is enabled? print(" 'xmpps-server' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not s2s_tls_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown Direct TLS server port: "..record.srv.port); end end else print(" No _xmpps-server SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; end end if target_hosts:empty() then target_hosts:add(host); end if target_hosts:contains("localhost") then print(" Target 'localhost' cannot be accessed from other servers"); target_hosts:remove("localhost"); end if modules:contains("proxy65") then local proxy65_target = configmanager.get(host, "proxy65_address") or host; if type(proxy65_target) == "string" then local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA"); local prob = {}; if not A then table.insert(prob, "A"); end if v6_supported and not AAAA then table.insert(prob, "AAAA"); end if #prob > 0 then print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/") .." record. Create one or set 'proxy65_address' to the correct host/IP."); end else print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check"); end end local use_ipv4 = configmanager.get("*", "use_ipv4") ~= false; local use_ipv6 = configmanager.get("*", "use_ipv6") ~= false; if not use_ipv4 and not use_ipv6 then print(" Both IPv6 and IPv4 are disabled, Prosody will not listen on any ports"); print(" nor be able to connect to any remote servers."); all_targets_ok = false; end for target_host in target_hosts do local host_ok_v4, host_ok_v6; do local res = dns.lookup(idna.to_ascii(target_host), "A"); if res then for _, record in ipairs(res) do if external_addresses:contains(record.a) then some_targets_ok = true; host_ok_v4 = true; elseif internal_addresses:contains(record.a) then host_ok_v4 = true; some_targets_ok = true; print(" "..target_host.." A record points to internal address, external connections might fail"); else print(" "..target_host.." A record points to unknown address "..record.a); all_targets_ok = false; end end end end do local res = dns.lookup(idna.to_ascii(target_host), "AAAA"); if res then for _, record in ipairs(res) do if external_addresses:contains(record.aaaa) then some_targets_ok = true; host_ok_v6 = true; elseif internal_addresses:contains(record.aaaa) then host_ok_v6 = true; some_targets_ok = true; print(" "..target_host.." AAAA record points to internal address, external connections might fail"); else print(" "..target_host.." AAAA record points to unknown address "..record.aaaa); all_targets_ok = false; end end end end if host_ok_v4 and not use_ipv4 then print(" Host "..target_host.." does seem to resolve to this server but IPv4 has been disabled"); all_targets_ok = false; end if host_ok_v6 and not use_ipv6 then print(" Host "..target_host.." does seem to resolve to this server but IPv6 has been disabled"); all_targets_ok = false; end local bad_protos = {} if use_ipv4 and not host_ok_v4 then table.insert(bad_protos, "IPv4"); end if use_ipv6 and not host_ok_v6 then table.insert(bad_protos, "IPv6"); end if #bad_protos > 0 then print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")"); end if host_ok_v6 and not v6_supported then print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6."); print(" Please see https://prosody.im/doc/ipv6 for more information."); end end if not all_targets_ok then print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server."); if is_component then print(" DNS records are necessary if you want users on other servers to access this component."); end problem_hosts:add(host); end print(""); end if not problem_hosts:empty() then print(""); print("For more information about DNS configuration please see https://prosody.im/doc/dns"); print(""); ok = false; end end if not what or what == "certs" then local cert_ok; print"Checking certificates..." local x509_verify_identity = require"util.x509".verify_identity; local create_context = require "core.certmanager".create_context; local ssl = dependencies.softreq"ssl"; -- local datetime_parse = require"util.datetime".parse_x509; local load_cert = ssl and ssl.loadcertificate; -- or ssl.cert_from_pem if not ssl then print("LuaSec not available, can't perform certificate checks") if what == "certs" then cert_ok = false end elseif not load_cert then print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking"); cert_ok = false else local function skip_bare_jid_hosts(host) if jid_split(host) then -- See issue #779 return false; end return true; end for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do print("Checking certificate for "..host); -- First, let's find out what certificate this host uses. local host_ssl_config = configmanager.rawget(host, "ssl") or configmanager.rawget(host:match("%.(.*)"), "ssl"); local global_ssl_config = configmanager.rawget("*", "ssl"); local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config); if not ok then print(" Error: "..err); cert_ok = false elseif not ssl_config.certificate then print(" No 'certificate' found for "..host) cert_ok = false elseif not ssl_config.key then print(" No 'key' found for "..host) cert_ok = false else local key, err = io.open(ssl_config.key); -- Permissions check only if not key then print(" Could not open "..ssl_config.key..": "..err); cert_ok = false else key:close(); end local cert_fh, err = io.open(ssl_config.certificate); -- Load the file. if not cert_fh then print(" Could not open "..ssl_config.certificate..": "..err); cert_ok = false else print(" Certificate: "..ssl_config.certificate) local cert = load_cert(cert_fh:read"*a"); cert_fh:close(); if not cert:validat(os.time()) then print(" Certificate has expired.") cert_ok = false elseif not cert:validat(os.time() + 86400) then print(" Certificate expires within one day.") cert_ok = false elseif not cert:validat(os.time() + 86400*7) then print(" Certificate expires within one week.") elseif not cert:validat(os.time() + 86400*31) then print(" Certificate expires within one month.") end if configmanager.get(host, "component_module") == nil and not x509_verify_identity(host, "_xmpp-client", cert) then print(" Not valid for client connections to "..host..".") cert_ok = false end if (not (configmanager.get(host, "anonymous_login") or configmanager.get(host, "authentication") == "anonymous")) and not x509_verify_identity(host, "_xmpp-server", cert) then print(" Not valid for server-to-server connections to "..host..".") cert_ok = false end end end end end if cert_ok == false then print("") print("For more information about certificates please see https://prosody.im/doc/certificates"); ok = false end print("") end if not ok then print("Problems found, see above."); else print("All checks passed, congratulations!"); end return ok and 0 or 2; end return { check = check; };