local typedefs = require "kong.db.schema.typedefs" local reserved_words = require "kong.plugins.acme.reserved_words" local redis_schema = require "kong.tools.redis.schema" local shallow_copy = require("kong.tools.table").shallow_copy local CERT_TYPES = { "rsa", "ecc" } local RSA_KEY_SIZES = { 2048, 3072, 4096 } local STORAGE_TYPES = { "kong", "shm", "redis", "consul", "vault" } local function validate_namespace(namespace) if namespace ~= "" then for _, v in pairs(reserved_words) do if namespace:sub(1, #v) == v then return nil, "namespace can't be prefixed with reserved word: " .. v end end end return true end local SHM_STORAGE_SCHEMA = { { shm_name = { description = "Name of shared memory zone used for Kong API gateway storage", type = "string", default = "kong", custom_validator = function(d) return ngx.shared[d] end, }, }, } -- must be a table per schema definition local KONG_STORAGE_SCHEMA = { } local LEGACY_SCHEMA_TRANSLATIONS = { { auth = { type = "string", len_min = 0, deprecation = { replaced_with = { { path = { 'password' } } }, message = "acme: config.storage_config.redis.auth is deprecated, please use config.storage_config.redis.password instead", removal_in_version = "4.0", }, func = function(value) return { password = value } end }}, { ssl_server_name = { type = "string", deprecation = { replaced_with = { { path = { 'server_name' } } }, message = "acme: config.storage_config.redis.ssl_server_name is deprecated, please use config.storage_config.redis.server_name instead", removal_in_version = "4.0", }, func = function(value) return { server_name = value } end }}, { namespace = { type = "string", len_min = 0, deprecation = { replaced_with = { { path = { 'extra_options', 'namespace' } } }, message = "acme: config.storage_config.redis.namespace is deprecated, please use config.storage_config.redis.extra_options.namespace instead", removal_in_version = "4.0", }, func = function(value) return { extra_options = { namespace = value } } end }}, { scan_count = { type = "integer", deprecation = { replaced_with = { { path = { 'extra_options', 'scan_count' } } }, message = "acme: config.storage_config.redis.scan_count is deprecated, please use config.storage_config.redis.extra_options.scan_count instead", removal_in_version = "4.0", }, func = function(value) return { extra_options = { scan_count = value } } end }}, } local REDIS_STORAGE_SCHEMA = shallow_copy(redis_schema.config_schema.fields) table.insert(REDIS_STORAGE_SCHEMA, { extra_options = { description = "Custom ACME Redis options", type = "record", fields = { { namespace = { type = "string", description = "A namespace to prepend to all keys stored in Redis.", required = true, default = "", len_min = 0, custom_validator = validate_namespace } }, { scan_count = { type = "number", required = false, default = 10, description = "The number of keys to return in Redis SCAN calls." } }, } } }) local CONSUL_STORAGE_SCHEMA = { { https = { type = "boolean", default = false, description = "Boolean representation of https."}, }, { host = typedefs.host}, { port = typedefs.port}, { kv_path = { type = "string", description = "KV prefix path."}, }, { timeout = { type = "number", description = "Timeout in milliseconds."}, }, { token = { type = "string", referenceable = true, description = "Consul ACL token."}, }, } local VAULT_STORAGE_SCHEMA = { { https = { type = "boolean", default = false, description = "Boolean representation of https." }, }, { host = typedefs.host, }, { port = typedefs.port, }, { kv_path = { type = "string", description = "KV prefix path." }, }, { timeout = { type = "number", description = "Timeout in milliseconds."}, }, { token = { type = "string", referenceable = true, description = "Consul ACL token." }, }, { tls_verify = { type = "boolean", default = true, description = "Turn on TLS verification." }, }, { tls_server_name = { type = "string", description = "SNI used in request, default to host if omitted." }, }, { auth_method = { type = "string", default = "token", one_of = { "token", "kubernetes" }, description = "Auth Method, default to token, can be 'token' or 'kubernetes'." } }, { auth_path = { type = "string", description = "Vault's authentication path to use." }, }, { auth_role = { type = "string", description = "The role to try and assign." }, }, { jwt_path = { type = "string", description = "The path to the JWT." }, }, } local ACCOUNT_KEY_SCHEMA = { { key_id = { type = "string", required = true, description = "The Key ID." } }, { key_set = { type = "string", description = "The ID of the key set to associate the Key ID with." } } } local schema = { name = "acme", fields = { -- global plugin only { consumer = typedefs.no_consumer }, { service = typedefs.no_service }, { route = typedefs.no_route }, { protocols = typedefs.protocols_http }, { config = { type = "record", fields = { { account_email = { description = "The account identifier. Can be reused in a different plugin instance.", type = "string", -- very loose validation for basic sanity test match = "%w*%p*@+%w*%.?%w*", required = true, encrypted = true, -- Kong Enterprise-exclusive feature, does nothing in Kong CE referenceable = true, }, }, { account_key = { description = "The private key associated with the account.", type = "record", required = false, fields = ACCOUNT_KEY_SCHEMA, }, }, { api_uri = typedefs.url({ default = "https://acme-v02.api.letsencrypt.org/directory" }), }, { tos_accepted = { type = "boolean", description = "If you are using Let's Encrypt, you must set this to `true` to agree the terms of service.", default = false, }, }, { eab_kid = { description = "External account binding (EAB) key id. You usually don't need to set this unless it is explicitly required by the CA.", type = "string", encrypted = true, -- Kong Enterprise-exclusive feature, does nothing in Kong CE referenceable = true, }, }, { eab_hmac_key = { description = "External account binding (EAB) base64-encoded URL string of the HMAC key. You usually don't need to set this unless it is explicitly required by the CA.", type = "string", encrypted = true, -- Kong Enterprise-exclusive feature, does nothing in Kong CE referenceable = true, }, }, -- Kong doesn't support multiple certificate chains yet { cert_type = { description = "The certificate type to create. The possible values are `rsa` for RSA certificate or `ecc` for EC certificate.", type = "string", default = 'rsa', one_of = CERT_TYPES, }, }, { rsa_key_size = { description = "RSA private key size for the certificate. The possible values are 2048, 3072, or 4096.", type = "number", default = 4096, one_of = RSA_KEY_SIZES, }, }, { renew_threshold_days = { description = "Days remaining to renew the certificate before it expires.", type = "number", default = 14, }, }, { domains = typedefs.hosts }, { allow_any_domain = { description = "If set to `true`, the plugin allows all domains and ignores any values in the `domains` list.", type = "boolean", default = false, }, }, { fail_backoff_minutes = { description = "Minutes to wait for each domain that fails to create a certificate. This applies to both a\nnew certificate and a renewal certificate.", type = "number", default = 5, }, }, { storage = { description = "The backend storage type to use. In DB-less mode and Konnect, `kong` storage is unavailable. In hybrid mode and Konnect, `shm` storage is unavailable. `shm` storage does not persist during Kong restarts and does not work for Kong running on different machines, so consider using one of `kong`, `redis`, `consul`, or `vault` in production.", type = "string", default = "shm", one_of = STORAGE_TYPES, }, }, { storage_config = { type = "record", fields = { { shm = { type = "record", fields = SHM_STORAGE_SCHEMA, } }, { kong = { type = "record", fields = KONG_STORAGE_SCHEMA, } }, { redis = { type = "record", fields = REDIS_STORAGE_SCHEMA, shorthand_fields = LEGACY_SCHEMA_TRANSLATIONS } }, { consul = { type = "record", fields = CONSUL_STORAGE_SCHEMA, } }, { vault = { type = "record", fields = VAULT_STORAGE_SCHEMA, } }, }, }, }, { preferred_chain = { description = "A string value that specifies the preferred certificate chain to use when generating certificates.", type = "string", }, }, { enable_ipv4_common_name = { description = "A boolean value that controls whether to include the IPv4 address in the common name field of generated certificates.", type = "boolean", default = true, }, }, }, }, }, }, entity_checks = { { conditional = { if_field = "config.api_uri", if_match = { one_of = { "https://acme-v02.api.letsencrypt.org", "https://acme-staging-v02.api.letsencrypt.org", } }, then_field = "config.tos_accepted", then_match = { eq = true }, then_err = "terms of service must be accepted, see https://letsencrypt.org/repository/", } }, { conditional = { if_field = "config.storage", if_match = { eq = "redis" }, then_field = "config.storage_config.redis.host", then_match = { required = true }, } }, { conditional = { if_field = "config.storage", if_match = { eq = "redis" }, then_field = "config.storage_config.redis.port", then_match = { required = true }, } }, { custom_entity_check = { field_sources = { "config.storage", }, fn = function(entity) local field = entity.config.storage if _G.kong and kong.configuration.database == "off" and kong.configuration.role ~= "data_plane" and field == "kong" then return nil, "\"kong\" storage can't be used with dbless mode" end if _G.kong and kong.configuration.role == "control_plane" and field == "shm" then return nil, "\"shm\" storage can't be used in Hybrid mode" end return true end } }, }, } return schema