GithubHelp home page GithubHelp logo

fffonion / lua-resty-acme Goto Github PK

View Code? Open in Web Editor NEW
150.0 6.0 36.0 449 KB

Automatic Let's Encrypt certificate serving and Lua implementation of ACMEv2 procotol

Lua 53.20% Makefile 0.41% Perl 43.09% Shell 3.30%
openresty acme acme-v2 acme-lua letsencrypt

lua-resty-acme's Introduction

lua-resty-acme

Automatic Let's Encrypt certificate serving (RSA + ECC) and pure Lua implementation of the ACMEv2 protocol.

http-01 and tls-alpn-01 challenges are supported.

Build Status luarocks opm

简体中文

Table of Contents

Description

This library consists of two parts:

  • resty.acme.autossl: automatic lifecycle management of Let's Encrypt certificates
  • resty.acme.client: Lua implementation of ACME v2 protocol

Install using opm:

opm install fffonion/lua-resty-acme

Alternatively, to install using luarocks:

luarocks install lua-resty-acme
# manually install a luafilesystem
luarocks install luafilesystem

Note you will need to manually install luafilesystem when using LuaRocks. This is made to maintain backward compatibility.

This library uses an FFI-based openssl backend, which currently supports OpenSSL 1.1.1, 1.1.0 and 1.0.2 series.

Back to TOC

Status

Production.

Synopsis

Create account private key and fallback certs:

# create account key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /etc/openresty/account.key
# create fallback cert and key
openssl req -newkey rsa:2048 -nodes -keyout /etc/openresty/default.key -x509 -days 365 -out /etc/openresty/default.pem

Use the following example config:

events {}

http {
    resolver 8.8.8.8 ipv6=off;

    lua_shared_dict acme 16m;

    # required to verify Let's Encrypt API
    lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    lua_ssl_verify_depth 2;

    init_by_lua_block {
        require("resty.acme.autossl").init({
            -- setting the following to true
            -- implies that you read and accepted https://letsencrypt.org/repository/
            tos_accepted = true,
            -- uncomment following for first time setup
            -- staging = true,
            -- uncomment following to enable RSA + ECC double cert
            -- domain_key_types = { 'rsa', 'ecc' },
            -- uncomment following to enable tls-alpn-01 challenge
            -- enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
            account_key_path = "/etc/openresty/account.key",
            account_email = "[email protected]",
            domain_whitelist = { "example.com" },
        })
    }

    init_worker_by_lua_block {
        require("resty.acme.autossl").init_worker()
    }

    server {
        listen 80;
        listen 443 ssl;
        server_name example.com;

        # fallback certs, make sure to create them before hand
        ssl_certificate /etc/openresty/default.pem;
        ssl_certificate_key /etc/openresty/default.key;

        ssl_certificate_by_lua_block {
            require("resty.acme.autossl").ssl_certificate()
        }

        location /.well-known {
            content_by_lua_block {
                require("resty.acme.autossl").serve_http_challenge()
            }
        }
    }
}

When testing deployment, it's recommanded to uncomment the staging = true to allow an end-to-end test of your environment. This can avoid configuration failure result into too many requests that hits rate limiting on Let's Encrypt API.

By default autossl only creates RSA certificates. To use ECC certificates or both, uncomment domain_key_types = { 'rsa', 'ecc' }. Note that multiple certificate chain is only supported by NGINX 1.11.0 or later.

A certificate will be queued to create after Nginx seen request with such SNI, which might take tens of seconds to finish. During the meantime, requests with such SNI are responsed with the fallback certificate.

Note that domain_whitelist or domain_whitelist_callback must be set to include your domain that you wish to server autossl, to prevent potential abuse using fake SNI in SSL handshake. domain_whitelist defines a table that includes all domains should be included, and domain_whitelist_callback defines a function that accepts domain as parameter and return boolean to indicate if it should be included.

domain_whitelist = { "domain1.com", "domain2.com", "domain3.com" },

To match a pattern in your domain name, for example all subdomains under example.com, use:

domain_whitelist_callback = function(domain, is_new_cert_needed)
    return ngx.re.match(domain, [[\.example\.com$]], "jo")
end

Furthermore, since checking domain whitelist is running in certificate phase. It's possible to use cosocket API here. Do note that this will increase the SSL handshake latency.

domain_whitelist_callback = function(domain, is_new_cert_needed)
    -- send HTTP request
    local http = require("resty.http")
    local res, err = httpc:request_uri("http://example.com")
    -- access the storage
    local acme = require("resty.acme.autossl")
    local value, err = acme.storage:get("key")
    -- get cert from resty LRU cache
    -- cached = { pkey, cert } or nil if cert is not in cache
    local cached, staled, flags = acme.get_cert_from_cache(domain, "rsa")
    -- do something to check the domain
    -- return is_domain_included
end}),

domain_whitelist_callback function is provided with a second argument, which indicates whether the certificate is about to be served on incoming HTTP request (false) or new certificate is about to be requested (true). This allows to use cached values on hot path (serving requests) while fetching fresh data from storage for new certificates. One may also implement different logic, e.g. do extra checks before requesting new cert.

In case of certificate request failure one may want to prevent ACME client to request another certificate immediatelly. By default, the cooloff period it is set to 300 seconds (5 minutes). It may be customized with failure_cooloff or with failure_cooloff_callback function, e.g. to implement exponential backoff.

    failure_cooloff_callback = function(domain, count)
      if count == 1 then
        return 600 -- 10 minutes
      elseif count == 2 then
        return 1800 -- 30 minutes
      elseif count == 3 then
        return 3600 -- 1 hour
      elseif count == 4 then
        return 43200 -- 12 hours
      elseif count == 5 then
        return 43200 -- 12 hours
      else
        return 86400 -- 24 hours
      end
    end

tls-alpn-01 challenge

tls-alpn-01 challenge is currently supported on Openresty 1.15.8.x, 1.17.8.x and 1.19.3.x.

Click to expand sample config
events {}

http {
    resolver 8.8.8.8 ipv6=off;

    lua_shared_dict acme 16m;

    # required to verify Let's Encrypt API
    lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    lua_ssl_verify_depth 2;

    init_by_lua_block {
        require("resty.acme.autossl").init({
            -- setting the following to true
            -- implies that you read and accepted https://letsencrypt.org/repository/
            tos_accepted = true,
            -- uncomment following for first time setup
            -- staging = true,
            -- uncomment folloing to enable RSA + ECC double cert
            -- domain_key_types = { 'rsa', 'ecc' },
            -- uncomment following to enable tls-alpn-01 challenge
            enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
            account_key_path = "/etc/openresty/account.key",
            account_email = "[email protected]",
            domain_whitelist = { "example.com" },
            storage_adapter = "file",
        })
    }
    init_worker_by_lua_block {
        require("resty.acme.autossl").init_worker()
    }

    server {
        listen 80;
        listen unix:/tmp/nginx-default.sock ssl;
        # listen unix:/tmp/nginx-default.sock ssl proxy_protocol;
        server_name example.com;

        # set_real_ip_from unix:;
        # real_ip_header proxy_protocol;

        # fallback certs, make sure to create them before hand
        ssl_certificate /etc/openresty/default.pem;
        ssl_certificate_key /etc/openresty/default.key;

        ssl_certificate_by_lua_block {
            require("resty.acme.autossl").ssl_certificate()
        }

        location /.well-known {
            content_by_lua_block {
                require("resty.acme.autossl").serve_http_challenge()
            }
        }
    }
}

stream {
    init_worker_by_lua_block {
        require("resty.acme.autossl").init({
            -- setting the following to true
            -- implies that you read and accepted https://letsencrypt.org/repository/
            tos_accepted = true,
            -- uncomment following for first time setup
            -- staging = true,
            -- uncomment folloing to enable RSA + ECC double cert
            -- domain_key_types = { 'rsa', 'ecc' },
            -- uncomment following to enable tls-alpn-01 challenge
            enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
            account_key_path = "/etc/openresty/account.key",
            account_email = "[email protected]",
            domain_whitelist = { "example.com" },
            storage_adapter = "file"
        })
        require("resty.acme.autossl").init_worker()
    }

    map $ssl_preread_alpn_protocols $backend {
        ~\bacme-tls/1\b unix:/tmp/nginx-tls-alpn.sock;
        default unix:/tmp/nginx-default.sock;
    }

    server {
            listen 443;
            listen [::]:443;

            ssl_preread on;
            proxy_pass $backend;

            # proxy_protocol on;
    }

    server {
            listen unix:/tmp/nginx-tls-alpn.sock ssl;
            # listen nix:/tmp/nginx-tls-alpn.sock ssl proxy_protocol;
            ssl_certificate certs/default.pem;
            ssl_certificate_key certs/default.key;

            # requires --with-stream_realip_module
            # set_real_ip_from unix:;

            ssl_certificate_by_lua_block {
                    require("resty.acme.autossl").serve_tls_alpn_challenge()
            }

            content_by_lua_block {
                    ngx.exit(0)
            }
    }
}

In the above sample config, we set a http server and two stream server.

The very front stream server listens for 443 port and route to different upstream based on client ALPN. The tls-alpn-01 responder listens on unix:/tmp/nginx-tls-alpn.sock. All normal https traffic listens on unix:/tmp/nginx-default.sock.

                                                [stream server unix:/tmp/nginx-tls-alpn.sock ssl]
                                            Y /
[stream server 443] --- ALPN is acme-tls ?
                                            N \
                                                [http server unix:/tmp/nginx-default.sock ssl]
  • The config passed to require("resty.acme.autossl").init in both subsystem should be kept same as possible.
  • tls-alpn-01 challenge handler doesn't need any third party dependency.
  • You can enable http-01 and tls-alpn-01 challenge handlers at the same time.
  • http and stream subsystem doesn't share shm, thus considering use a storage other than shm. If you must use shm, you will need to apply this patch.

resty.acme.autossl

A config table can be passed to resty.acme.autossl.init(), the default values are:

default_config = {
  -- accept term of service https://letsencrypt.org/repository/
  tos_accepted = false,
  -- if using the let's encrypt staging API
  staging = false,
  -- the path to account private key in PEM format
  account_key_path = nil,
  -- the account email to register
  account_email = nil,
  -- number of certificate cache, per type
  cache_size = 100,
  domain_key_paths = {
    -- the global domain RSA private key
    rsa = nil,
    -- the global domain ECC private key
    ecc = nil,
  },
  -- the private key algorithm to use, can be one or both of
  -- 'rsa' and 'ecc'
  domain_key_types = { 'rsa' },
  -- restrict registering new cert only with domain defined in this table
  domain_whitelist = nil,
  -- restrict registering new cert only with domain checked by this function
  domain_whitelist_callback = nil,
  -- interval to wait before retrying after failed certificate request
  failure_cooloff = 300,
  -- function that returns interval to wait before retrying after failed certificate request
  failure_cooloff_callback = nil,
  -- the threshold to renew a cert before it expires, in seconds
  renew_threshold = 7 * 86400,
  -- interval to check cert renewal, in seconds
  renew_check_interval = 6 * 3600,
  -- the store certificates
  storage_adapter = "shm",
  -- the storage config passed to storage adapter
  storage_config = {
    shm_name = 'acme',
  },
  -- the challenge types enabled
  enabled_challenge_handlers = { 'http-01' },
  -- time to wait before signaling ACME server to validate in seconds
  challenge_start_delay = 0,
  -- if true, the request to nginx waits until the cert has been generated and it is used right away
  blocking = false,
}

If account_key_path is not specified, a new account key will be created everytime Nginx reloads configuration. Note this may trigger New Account rate limiting on Let's Encrypt API.

If domain_key_paths is not specified, a new private key will be generated for each certificate (4096-bits RSA and 256-bits prime256v1 ECC). Note that generating such key will block worker and will be especially noticable on VMs where entropy is low.

Pass config table directly to ACME client as second parameter. The following example demonstrates how to use a CA provider other than Let's Encrypt and also set the preferred chain.

resty.acme.autossl.init({
    tos_accepted = true,
    account_email = "[email protected]",
  }, {
    api_uri = "https://acme.otherca.com/directory",
    preferred_chain = "OtherCA PKI Root CA",
  }
)

See also Storage Adapters below.

When using distributed storage types, it's useful to bump up challenge_start_delay to allow changes in storage to propogate around. When challenge_start_delay is set to 0, no wait will be performed before start validating challenges.

autossl.get_certkey

syntax: certkey, err = autossl.get_certkey(domain, type?)

Return the PEM-encoded certificate and private key for domain from storage. Optionally accepts a type parameter which can be "rsa" or "ecc"; if omitted, type will default to "rsa".

Back to TOC

resty.acme.client

client.new

syntax: c, err = client.new(config)

Create a ACMEv2 client.

Default values for config are:

default_config = {
  -- the ACME v2 API endpoint to use
  api_uri = "https://acme-v02.api.letsencrypt.org/directory",
  -- the account email to register
  account_email = nil,
  -- the account key in PEM format text
  account_key = nil,
  -- the account kid (as an URL)
  account_kid = nil,
  -- external account binding key id
  eab_kid = nil,
  -- external account binding hmac key, base64url encoded
  eab_hmac_key = nil,
  -- external account registering handler
  eab_handler = nil,
  -- storage for challenge
  storage_adapter = "shm",
  -- the storage config passed to storage adapter
  storage_config = {
    shm_name = "acme"
  },
  -- the challenge types enabled, selection of `http-01` and `tls-alpn-01`
  enabled_challenge_handlers = {"http-01"},
  -- select preferred root CA issuer's Common Name if appliable
  preferred_chain = nil,
  -- callback function that allows to wait before signaling ACME server to validate
  challenge_start_callback = nil,
}

If account_kid is omitted, user must call client:new_account() to register a new account. Note that when using the same account_key, client:new_account() will return the same kid that is previosuly registered.

If CA requires External Account Binding, user can set eab_kid and eab_hmac_key to load an existing account, or set account_email and eab_handler to register a new account. eab_hmac_key must be base64 url encoded. In later case, user must call client:new_account() to register a new account. eab_handler must be an function that accepts account_email as parameter and returns eab_kid, eab_hmac_key and error if any.

eab_handler = function(account_email)
  -- do something to register an account with account_email
  -- if err then
  --  return nil, nil, err
  -- end
  return eab_kid, eab_hmac_key
end

The following CA provider's EAB handler is supported by lua-resty-acme and user doesn't need to implement their own eab_handler:

preferred_chain is used to select a chain with matching Common Name in its root CA. For example, user can use use "ISRG Root X1" to force use the new default chain in Let's Encrypt. When no value is configured or the configured name is not found in any chain, the default chain will be used.

challenge_start_callback is a callback function to allow the client to wait before signalling ACME server to start validate challenge. It's useful in a distributed setup where challenges take time to propogate. challenge_start_callback accepts challenge_type and challenge_token. The client calls this function every second until it returns true indicating challenge should start; if this challenge_start_callback is not set, no wait will be performed.

challenge_start_callback = function(challenge_type, challenge_token)
  -- do something here
  -- if we are good
  return true
end

See also Storage Adapters below.

Back to TOC

client:init

syntax: err = client:init()

Initialize the client, requires availability of cosocket API. This function will login or register an account.

Back to TOC

client:order_certificate

syntax: err = client:order_certificate(domain,...)

Create a certificate with one or more domains. Note that wildcard domains are not supported as it can only be verified by dns-01 challenge.

Back to TOC

client:serve_http_challenge

syntax: client:serve_http_challenge()

Serve http-01 challenge. A common use case will be to put this as a content_by_* block for /.well-known path.

Back to TOC

client:serve_tls_alpn_challenge

syntax: client:serve_tls_alpn_challenge()

Serve tls-alpn-01 challenge. See this section on how to use this handler.

Back to TOC

Storage Adapters

Storage adapters are used in autossl or acme client to storage temporary or persistent data. Depending on the deployment environment, there're currently five storage adapters available to select from. To implement a custom storage adapter, please refer to this doc.

file

Filesystem based storage. Sample configuration:

storage_config = {
    dir = '/etc/openresty/storage',
}

If dir is omitted, the OS temporary directory will be used.

luafilesystem or luafilesystem-ffi is needed when using the file storage for renewal.

shm

Lua shared dict based storage. Note this storage is volatile between Nginx restarts (not reloads). Sample configuration:

storage_config = {
    shm_name = 'dict_name',
}

redis

Redis based storage. The default config is:

storage_config = {
    host = '127.0.0.1',
    port = 6379,
    database = 0,
    -- Redis authentication key
    auth = nil,
    ssl = false,
    ssl_verify = false,
    ssl_server_name = nil,
    -- namespace as a prefix of key
    namespace = "",
}

Redis >= 2.6.0 is required as this storage requires PEXPIRE.

vault

Hashicorp Vault based storage. Only KV V2 backend is supported. The default config is:

storage_config = {
    host = '127.0.0.1',
    port = 8200,
    -- secrets kv prefix path
    kv_path = "acme",
    -- timeout in ms
    timeout = 2000,
    -- use HTTPS
    https = false,
    -- turn on tls verification
    tls_verify = true
    -- SNI used in request, default to host if omitted
    tls_server_name = nil,
    -- Auth Method, default to token, can be "token" or "kubernetes"
    auth_method = "token"
    -- Vault token
    token = nil,
    -- Vault's authentication path to use
    auth_path =  "kubernetes",
    -- The role to try and assign
    auth_role = nil,
    -- The path to the JWT
    jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token",
    -- Vault namespace
    namespace = nil,
}

Support for different auth method

  • Token: This is the default and allows to pass a literal "token" in the configuration

  • Kubernetes: Via this method, one can utilize vault's built-in auth method for kubernetes What this basically this is take the service account token and validates it has been signed by Kubernetes CA. The major benefit here, is that config files don't expose your token anymore.

    The following configurations apply here:

      -- Vault's authentication path to use
      auth_path =  "kubernetes",
      -- The role to try and assign
      auth_role = nil,
      -- The path to the JWT
      jwt_path = "/var/run/secrets/kubernetes.io/serviceaccount/token",

consul

Hashicorp Consul based storage. The default config is:

storage_config = {
    host = '127.0.0.1',
    port = 8500,
    -- kv prefix path
    kv_path = "acme",
    -- Consul ACL token
    token = nil,
    -- timeout in ms
    timeout = 2000,
}

etcd

etcd based storage. Right now only v2 protocol is supported. The default config is:

storage_config = {
    http_host = 'http://127.0.0.1:4001',
    protocol = 'v2',
    key_prefix = '',
    timeout = 60,
    ssl_verify = false,
}

Etcd storage requires lua-resty-etcd library to installed. It can be manually installed with opm install api7/lua-resty-etcd or luarocks install lua-resty-etcd.

TODO

  • autossl: ocsp staping

Back to TOC

Testing

Setup e2e test environment by running bash t/fixtures/prepare_env.sh.

Then run cpanm install Test::Nginx::Socket and then prove -r t.

Back to TOC

Credits

  • Improvements of file storage by @dbalagansky
  • Addition of kubernetes auth in 'vault' storage by @UXabre

Copyright and License

This module is licensed under the BSD license.

Copyright (C) 2019, by fffonion [email protected].

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Back to TOC

See Also

Back to TOC

lua-resty-acme's People

Contributors

add-sp avatar catbro666 avatar cauboy avatar cdloh avatar chewi avatar dbalagansky avatar doobled avatar fffonion avatar jynolen avatar kfigiela avatar nizar-m avatar pr4u4t avatar szesch avatar uxabre avatar vm-001 avatar yuweizzz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

lua-resty-acme's Issues

whitelist not working

I have configured package according to the docs and yet any domain i point to this server gets a certificate

    resolver 127.0.0.53 ipv6=off;

    init_by_lua_block {
        require("resty.acme.autossl").init({
            tos_accepted = true,
            enabled_challenge_handlers = { 'http-01', 'tls-alpn-01' },
            account_key_path = "/etc/openresty/account.key",
            account_email = "[email protected]",
            domain_whitelist = { "kust.domain.tld", "another.tld", "domain.tld" },
        })
    }

    init_worker_by_lua_block {
        require("resty.acme.autossl").init_worker()
    }

    include       mime.types;
    default_type  application/application-json;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;

    keepalive_timeout   65;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    access_log /var/log/openresty/access.log;
    error_log /var/log/openresty/error.log;

    gzip on;

server {
    listen 80;
    listen 443 ssl;
    server_name _;

    lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    lua_ssl_verify_depth 2;

    ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;

    ssl_certificate_by_lua_block {
        require("resty.acme.autossl").ssl_certificate()
    }

    location /.well-known {
        content_by_lua_block {
            require("resty.acme.autossl").serve_http_challenge()
        }
    }
    location / {
        proxy_pass http://10.110.0.2:3000;
    }
}

}

openresty v: 1.19.3.1 with opm

When I point diffferent.tld or sub.domain.tld to gateway it still gets certificate, besides not being in the whitelist.
what I might be doing wrong?

Option to delete none whitelisted domains in certificate update

In our usage of this library a domain which is in the whitelist might be removed from the whitelist later. We would appreciate it if the domain is then removed from the storage used by this library, too. This could be done in the certificate upate process.

In AUTOSSL.update_cert an error message is returned when a domain is not whitelisted. It would be nice to have an option that instead of returning an error message the domain is deleted from storage. That way a none whitlisted domain would not have to be checked every time the worker is running and there would not be unnecessary messages in the log.

Error while trying to use subdomain with "_" simbol

Hi,

I'm not sure where the problem is but when trying to whitelist and issue certificate for subdomain with underscore symbol _ issuing fails with the fallowing error:

[error] 23251#23251: *2978 [acme] autossl.lua:189: error updating cert for smart_panel.example.com err: failed to create new order: Error creating new order :: Cannot issue for "smart_panel.example.com ": Domain name contains an invalid character, context: ngx.timer, client ...

end whenever I drop _ symbol everything works as expected.

How to configure other storage configs when in DB-less?

Hello,

The documentation says that we can configure other storage, and points to this part:
https://docs.konghq.com/hub/kong-inc/acme/#storage-config

But I'm not sure how I'm supposed to write a record type in DB-less (yaml format).
I tried this:

plugins:
- name: acme
  config:
    account_email: mymail
    cert_type: ecc
    domains: ["*.mydomain"]
    tos_accepted: true
    storage: vault
    storage_config:
      host: myvault
      port: 443
      token: mytoken
      kv_path: mypath

but I got this parsing error:

2022/08/01 14:29:18 [error] 169559#0: init_by_lua error: /usr/local/share/lua/5.1/kong/init.lua:553: error parsing declarative config file /etc/kong/kong.yml:
in 'plugins':
  - in entry 1 of 'plugins':
    in 'config':
      in 'storage_config':
        in 'kv_path': unknown field
        in 'token': unknown field
        in 'host': unknown field
        in 'port': unknown field

Did I miss some part of the documentation?

Can I update domain_whitelist dynamically?

Can I add domains to domain_whitelist without restarting OpenResty or reloading the config?

I'm new to Lua but maybe since domain_whitelist is a "metatable", one can define an __index() function: (see http://lua-users.org/wiki/MetatableEvents )

__index - Control 'prototype' inheritance. When accessing "myTable[key]" and the key does not appear in the table, but the metatable has an __index property:
if the value is a function, the function is called, passing in the table and the key; the return value of that function is returned as the result

Could such an __index function use lua-resty-http and ask my app server if the new domain is allowed? Sth like:

domain_whitelist = setmetatable({}, { __index = function(_, k)
    return  lua-resty-http.askMyAppServerIfDomainAllowed(k)
end}),

***

The other project, lua-resty-autossl, has an allow-domain() callback, and apparently one can query Redis dynamically from it:

When using the Redis storage adapter, you can access the current Redis connection inside the allow_domain callback by accessing auto_ssl.storage.adapter:get_connection().

https://github.com/auto-ssl/lua-resty-auto-ssl#allow_domain

dns-01 challenge

对于泛解析证书来说, dns-01 是唯一的验证方式,是出于什么原因没有支持吗?

Consistent CI Tests

At present the Unit tests are a bit haphazard. Based off reading the latest logs quite a few of the problems appear to be around frp.

I had a few options on what could be done to make this more consistent / easier for people to join.

  1. Move to ngrok

Positives?

  • Most of the other autossl / LetEncrypt projects use this so makes it easier for people to move across projects (unlikely but handy?)
  • Users only have to set ngrok secrets on their forks rather then having to host a frp endpoint themselves.

Negatives

  • Only one test matrix can run at a time due to limitations on the free ngrok usage
  • Only one subdomain available for testing
  • Still requires people to create accounts
  1. Use a local ACME instance eg pebble as a service within the actions

Positives

  • Full control within the actions
  • No setup required from people using forks
  • Can do multiple domains using netalias within the docker network

Negatives

  • APIs can potentially be out of sync with main LE APIs

Maybe it would be best to do a combination of both.

Pebble sidecar runs on forks so no setup is required on people doing work for forks etc before creating MRs.
MRs run against the staging LE server using either frp or ngrok

Would like your thoughts on this and if you want to look at going down either of these paths @fffonion !

按照Synopsis的步骤操作之后,没有生效,访问网站发现使用的是fallback证书,如何查找原因呢?

我准备使用的域名是foo.example.com. 我期望的按官网配置部署到生产服务器上之后, 访问这个网站就应该显示lets encrypt的https.

我尝试源码打log,require("resty.acme.autossl").init是成功了的,比如

AUTOSSL.client

{
  /*7fb97f9d9ed8*/
  account_pkey      : {
    /*7fb97fb096f0*/
    buf            : "cdata<unsigned char [?]>: 0x7fb97fb25b28",
    buf_size       : 512,
    ctx            : "cdata<struct evp_pkey_st *>: 0x55f0fab6fb70",
    key_type       : 6,
    key_type_is_ecx: false,
  },
  account_thumbprint: "tOQBt1lI_7wTbeVzZlcWa6ImndYUA76kfvAUW3v9QJ8",
  challenge_handlers: {
    /*7fb97f9d9f20*/
    "http-01": {
      /*7fb97f9dba78*/
      storage   : {
        /*7fb97f9dabd0*/
        shm: [
          /*7fb9804e6958*/
          "userdata: 0x7fb9804e69e0",
        ],
      },
      uri_prefix: "acme-challenge",
    },
  },
  conf              : {
    /*7fb97f9d9a78*/
    account_email             : "[email protected]",
    account_key               : "-----BEGIN PRIVATE KEY-----\nbasdfsfds\n-----END PRIVATE KEY-----\n",
    api_uri                   : "https://acme-staging-v02.api.letsencrypt.org/directory",
    challenge_start_callback  : "function: 0x7fb97f9d9af8",
    enabled_challenge_handlers: [
      /*7fb97f9d8e40*/
      "http-01",
    ],
    storage_adapter           : "resty.acme.storage.shm",
    storage_config            : {
      /*7fb97f9d8dc0*/
      shm_name: "acme",
    },
  },
  eab_required      : false,
  storage           : {/*7fb97f9dabd0*/},
}

AUTOSSL.config

{
  /*7ff2fa6ba140*/
  account_email   : "[email protected]",
  account_key_path: "conf/account.key",
  domain_whitelist: {
    /*7ff2fa76c9c8*/
    1                : "foo.example.com",
    "foo.example.com": true,
  },
  staging         : true,
  storage_adapter : "resty.acme.storage.shm",
  tos_accepted    : true,
}

when using storage_adapter = redis requires a shm?

i use storage_adapter redis and trying to setup tls-alpn-01 challenge.
but at start I get an error:

stack traceback:
	[C]: in function 'error'
	...al/openresty/luajit/share/lua/5.1/resty/acme/autossl.lua:344: in function 'init'
	init_worker_by_lua:2: in main chunk

patch cross-module is needed anyway?

error during acme login

2022/08/29 10:45:32 [debug] 1143097#0: *11 [acme] client.lua:307: acme request: https://acme.zerossl.com/v2/DV90/newAccount response: {"type":"urn:ietf:params:acme:error:unauthorized","status":401,"detail":"The Request URL in the JWS Protected Header does not match the actual Request URL"}
2022/08/29 10:45:32 [error] 1143097#0: *11 [acme] autossl.lua:229: error during acme login: failed to create account: The Request URL in the JWS Protected Header does not match the actual Request URL, context: ngx.timer, client: 0.1.10.0, server: 0.0.0.0:443
2022/08/29 10:45:33 [debug] 1143097#0: *20 [acme] client.lua:307: acme request: https://acme.zerossl.com/v2/DV90/newAccount response: {"type":"urn:ietf:params:acme:error:unauthorized","status":401,"detail":"The Request URL in the JWS Protected Header does not match the actual Request URL"}
2022/08/29 10:45:33 [error] 1143097#0: *20 [acme] autossl.lua:229: error during acme login: failed to create account: The Request URL in the JWS Protected Header does not match the actual Request URL, context: ngx.timer, client: 0.1.10.0, server: 0.0.0.0:443
2022/08/29 10:45:33 [debug] 1143097#0: *16 [acme] client.lua:307: acme request: https://acme.zerossl.com/v2/DV90/newAccount response: {"type":"urn:ietf:params:acme:error:unauthorized","status":401,"detail":"The Request URL in the JWS Protected Header does not match the actual Request URL"}
2022/08/29 10:45:33 [error] 1143097#0: *16 [acme] autossl.lua:229: error during acme login: failed to create account: The Request URL in the JWS Protected Header does not match the actual Request URL, context: ngx.timer, client: 0.1.10.0, server: 0.0.0.0:443

How to use it?

Hi so I installed

luarocks install lua-resty-acme
luarocks install luafilesystem

also did everything here https://github.com/fffonion/lua-resty-acme#synopsis

but still don't get the certificate when I go to my website.

How do you use it? 😄

Do I need to run a command to get a SSL? I want to get SSL from zerossl.com because let's encrypt does not work.

My config:

events {}

http {
    resolver 8.8.8.8;

    lua_shared_dict acme 16m;

    # required to verify Let's Encrypt API
    lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    lua_ssl_verify_depth 2;
 
		init_by_lua_block {
		require("resty.acme.autossl").init({
		  -- you current configs
		tos_accepted = true,
		        account_key_path = "/etc/openresty/account.key",
		        account_email = "[email protected]",

		  eab_kid = "130jnnbfmhTA2-kCDWe5FAg",
		  -- external account binding hmac key, base64url encoded
		  eab_hmac_key = "15BVuaSD82WD2q3C5Q-l1Xn8X9L6m9i5jhlPMx3LTooiXr4xBoBwnD9mzf16JKdyzxrKKHGFaI6kZU5eE7hPcJA",
		        --domain_whitelist = { " yyy.com" }
		},
		{
		  api_url = "https://acme.zerossl.com/v2/DV90" 
		})}

   init_worker_by_lua_block {
        require("resty.acme.autossl").init_worker()
    }

    server {
        listen 80;
        listen 443 ssl;
        server_name yyy.com;
				location = /.well-known {
            content_by_lua_block {
                require("resty.acme.autossl").serve_http_challenge()
            }
        }
        # fallback certs, make sure to create them before hand
        ssl_certificate /etc/openresty/default.pem;
        ssl_certificate_key /etc/openresty/default.key;

        ssl_certificate_by_lua_block {
            require("resty.acme.autossl").ssl_certificate()
        }
    }


}

http-01 failed

I tried using curl -k -v to visit, it returns, but http status code 501.

f64f57fa93b1a00fdf2f670560eb49a4 117.143.104.231 [04/Jan/2022:22:02:59 +0800] TLSv1.2/ECDHE-RSA-AES256-GCM-SHA384 "GET / HTTP/2.0" 200 22879 "-" "xxx.yyy.com" curl/7.77.0 - 0.005 - - -
164be1733737f2b7420e80ad2cff68e9 34.221.255.206 [04/Jan/2022:22:03:01 +0800] -/- "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 301 162 "-" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.000 - - -
f9781deba06461ccec4b4732125d0f28 18.159.196.172 [04/Jan/2022:22:03:01 +0800] -/- "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 301 162 "-" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.000 - - -
d7f7a73dba67490c8c5a5124291b1070 3.19.56.43 [04/Jan/2022:22:03:02 +0800] -/- "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 301 162 "-" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.000 - - -
81fbbeb690b0021a1e53ea954463091d 66.133.109.36 [04/Jan/2022:22:03:02 +0800] -/- "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 301 162 "-" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.000 - - -
d15dcb78c5a27cc21b912e2683db09ec 34.221.255.206 [04/Jan/2022:22:03:03 +0800] TLSv1.3/TLS_AES_128_GCM_SHA256 "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 501 99 "http://xxx.yyy.com/.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.000 - - -
1a02f4abfe7ef999b8595c5b7bdaf709 3.19.56.43 [04/Jan/2022:22:03:03 +0800] TLSv1.3/TLS_AES_128_GCM_SHA256 "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 501 99 "http://xxx.yyy.com/.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.001 - - -
f45fb99fe0b352717f597f30fc0465e0 18.159.196.172 [04/Jan/2022:22:03:03 +0800] TLSv1.3/TLS_AES_128_GCM_SHA256 "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 501 99 "http://xxx.yyy.com/.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.001 - - -
6e2e5873255f61ab0493ce0c61342ca4 66.133.109.36 [04/Jan/2022:22:03:04 +0800] TLSv1.3/TLS_AES_128_GCM_SHA256 "GET /.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A HTTP/1.1" 501 99 "http://xxx.yyy.com/.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A" "xxx.yyy.com" Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org) - 0.001 - - -

==> /var/log/nginx/error.log <==
2022/01/04 22:03:06 [error] 8#8: *97 [acme] autossl.lua:182: error updating cert for xxx.yyy.com err: error checking challenge: challenge invalid: http-01: invalid: Invalid response from https://xxx.yyy.com/.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A [47.116.64.248]: 501, context: ngx.timer, client: 117.143.104.231, server: 0.0.0.0:443
2022/01/04 22:03:06 [error] 8#8: *97 [acme] autossl.lua:470: failed to create rsa certificate for domain xxx.yyy.com: error checking challenge: challenge invalid: http-01: invalid: Invalid response from https://xxx.yyy.com/.well-known/acme-challenge/hVTbVb1_VF7doKTvlk-myup0q4Cx_4kDN_ozlKa7f6A [47.116.64.248]: 501, context: ngx.timer, client: 117.143.104.231, server: 0.0.0.0:443

Differences from lua-resty-auto-ssl?

Hi Wangchong @fffonion

Nice with automatic https for Nginx / OpenResty :- )

Can I ask, 1) what are the differences between this project, lua-resty-acme and https://github.com/auto-ssl/lua-resty-auto-ssl ? And 2) what are the reasons this is a different project than lua-resty-auto-ssl (instead of building on lua-resty-auto-ssl)?

Some differences I noticed: This project can be installed using opm (OpenResty Package Manager), and supports more storages: Valut and Consul too. And this project supports tls-alpn-01 — maybe lua-resty-auto-ssl doesn't? (didn't see -alpn- mentioned in that project)

Change order in ssl_certificate

Hi,

We're looking at using this internally and I was wondering around the order of actions in ssl_certificate

At present you check if the certificate is whitelisted before serving the certificate.

Is there any reason why you don't swap those two actions around?

IE

  • check if you have a valid certificate, if you do serve it.
  • Check if it needs to be created (if it doesnt return)
  • If you does then check if it is whitelisted (bail out if it isnt)
  • Create it

This seems it would solve the issues around the slower times when using dynamic whitelists as it would only be checked if there isn't a valid certificate.

not work storage "file"

Hello
storage file not work

        storage_adapter = "file",
        storage_config = {
            dir = '/etc/openresty/storage',
        },

cert not get and not save.

tls-alpn-01 challenge is failing due to wrong certificate version

tls-alpn-01 challenge is currently failing with fhe following error.

challenge invalid: tls-alpn-01: invalid: Incorrect validation certificate for tls-alpn-01 challenge. 
Requested <domain> from <letsencrypt_ip>:443. Received 1 certificate(s), first certificate had names ""

The error location seems to be this in boulder:

The error is basically that it could not get any of the DNS Subject Alternetive Names. Boulder expects exactly one DNS SAN with the same name as the domain name.

When you look at the golang crypto/x509 code, the dns names will be parsed only if the version is 3.

We are not setting any version in tls-alpn-01.lua. Which means the version is 1. Setting the version as 3 might fix this issue

When you use openssl, you can clearly see the DNS SANs.

$ echo | openssl s_client -alpn acme-tls/1 -connect <domain>:443 2>/dev/null | openssl x509 -text -noout
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 0 (0x0)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: 
        Validity
            Not Before: Sep  3 06:00:21 2021 GMT
            Not After : Sep  3 06:00:21 2021 GMT
        Subject: 
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c5:83:b4:53:03:0d:1d:d8:70:75:fa:1c:c2:8b:
                    77:23:40:a5:3d:3b:e9:32:45:96:7e:88:19:3c:99:
                    77:d1:1f:af:52:58:27:c5:f6:d3:d9:b8:b9:3a:7d:
                    2e:13:3d:a4:2b:62:28:1c:b0:89:9c:b7:db:8c:5f:
                    64:3a:9e:58:06:ae:b6:a0:63:86:d8:72:f1:e2:e6:
                    a0:41:54:28:b0:cf:a4:f9:e5:48:e1:d3:51:e8:f0:
                    23:8f:58:7a:8d:77:33:80:bd:00:40:d8:4d:a1:a3:
                    81:43:45:a1:e4:36:3e:58:b5:ab:cb:3a:38:3c:81:
                    b4:bc:38:27:9b:b3:67:37:37:1d:aa:26:d6:63:14:
                    ad:45:ed:d9:f4:dd:9b:c8:db:a6:1e:ab:64:72:13:
                    5a:88:c9:e7:e2:0d:b8:a3:4f:58:c0:b2:b8:ed:45:
                    30:4c:e0:e4:e0:a1:50:1e:d0:f8:6f:8a:95:94:c5:
                    b5:a9:45:ac:e9:fa:61:3e:67:5d:19:e0:4f:fc:6b:
                    66:7d:96:87:7a:f3:a1:65:7f:5c:67:ac:d1:f3:66:
                    91:1d:2b:a2:49:ca:74:34:0e:ea:53:45:98:57:28:
                    49:3b:71:8d:ca:8a:00:85:cc:ba:54:c5:ab:30:75:
                    42:c8:75:fa:cc:6a:5d:c7:b9:84:2e:7c:a7:b2:b7:
                    29:9b
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                DNS:<domain>
            ..................: critical
                ...........................
    Signature Algorithm: sha256WithRSAEncryption
         59:5d:60:59:3c:31:a9:c5:db:84:e4:92:57:bd:d0:e8:42:53:
         9f:8d:75:cd:d4:ec:89:e9:66:95:35:a1:3a:5d:8d:b7:9c:2f:
         8e:86:f5:bc:0e:7a:bf:80:40:58:85:e3:30:d7:f8:f8:72:09:
         b0:48:8d:61:8a:47:db:43:8e:bd:9d:8b:58:c5:b2:82:b2:5e:
         24:9b:b7:dd:1d:a5:a8:93:8f:19:19:22:54:a6:e1:2d:4a:43:
         3c:13:0b:b4:e1:26:a6:59:10:d7:23:1e:05:76:a3:21:cb:33:
         dd:b0:a8:7a:e4:f1:d4:58:61:33:ac:22:79:62:99:94:35:11:
         93:ec:f6:24:06:68:25:73:75:a4:b4:46:04:97:81:26:76:ad:
         33:10:78:d3:e3:d9:88:aa:6a:24:7e:d4:bb:89:e1:b9:56:07:
         2e:20:f7:78:40:fc:0b:9d:7c:0c:88:85:0a:4c:d3:21:f4:1e:
         5e:37:4b:b4:16:a7:ad:1f:9e:83:ba:2d:38:cb:57:13:60:0b:
         a6:40:64:67:04:bb:69:df:10:ae:86:54:5b:37:3b:4c:6e:9c:
         ec:68:22:11:62:a6:31:1e:31:e9:93:78:47:51:9e:df:a2:fb:
         d9:98:a5:d1:9e:a9:a6:e2:19:40:8c:2f:a6:cb:be:ff:7c:91:
         d7:db:31:3a

Vault storage: Support kubernetes auth method

As one popular platform to use this plugin in is Kong, which in turn has excellent support for kubernetes, it should be made possible to allow storage in a vault KV secrets database by using your service account instead.

3 parameters should be definable:

  • role
  • auth path
  • serviceaccount file location

ZeroSSL integration failing to create account

Hi,

I am trying to invoke the lua-resty-acme library from kong using the acme plugin . I have been successfully using this workflow with LetsEncrypt for a long time now. Recently, I have started to hit rate limit concerns from letsencrypt and have been investigating switching over to a paid CA. My overall goal is to use this same kong + lua-resty-acme workflow with ZeroSSL. Howver, it is not working.

Originally I was specifying EAB credentials into client initialization acme.new. This was getting the error:

2022/06/27 16:13:17 [error] 20949#0: *1900 [kong] handler.lua:118 failed to update certificate: eab_handler undefined while EAB is required by CA, context: ngx.timer, client: 205.209.24.227, server: 0.0.0.0:443

I believe this is because I have provided the eab credentials in the config, this handler is unable to be loaded due to this condition here. Because the eab_handler is not set here, later in the execution flow, there is a condition that explicitly requires the eab handler here. This causes the error log that I am seeing.

Overall - I think this is because I am specifying EAB credentials and then also invoking the acme_client:new_account(). From the ZeroSSL discovery at https://acme.zerossl.com/v2/DV90, clearly "externalAccountRequired" = true and so I orignally thought that I needed to provied external EAB credentials. This was probably my initial misundersanding of the role of the EAB credentials.


In any case, I tried removed the eab_kid and eab_hmac_key from the acme client instantiation and instead let the account (and credentials) be created by the handler in the library.

From the logs, the zerossl eab handler is successfully creating eab credentials (I assume that these credentials are related to my account through the account_email though I am not sure.). The error occurs in the first request to the acme server that needs to use the credentials to create an account. Following the logs, the library injects some JWS stuff into the request which includes the credentials generated in the previous step.

2022/06/27 17:23:45 [debug] 30427#0: *802 [acme] client.lua:259: jws payload: {"payload":{"termsOfServiceAgreed":true,"contact":["mailto:[email protected]"],"externalAccountBinding":{"protected":"eyJraWQiOiJKUVNqQmVqNjVDdmdxdGhRZW1DTjRnIiwiYWxnIjoiSFMyNTYiLCJ1cmwiOiJodHRwczpcL1wvYWNtZS56ZXJvc3NsLmNvbVwvdjJcL0RWOTBcL25ld0FjY291bnQifQ","payload":"eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJyTlhzRFJWQU9DV1RKNU1ZbUY2am1RZTRNNmJyUUMxa0Iycng2RVZXd0xHZ1B6d3FkLUhzZEZzTFRPb0ZuU240Q0lnRXM4MXgzLXRlQkhVLVZ6dnRMOGNDYVR2RTB2THd6N0xKcEtHYnF1eThhZEJpSUJGUEJubTZaQVBLaDRUNFI0OVpwQTV5eTZiNDBaZ1BIMUJlNk5idXR4U2QwQVBWSlNYcjhlbWhKYTg4SkZKSW9LRDZyRGVRUU1tTnRLY2dRQnd6ZEpnSzlWVWR1OG5HUzJKTGFjZEFRQ3NpY1h0XzdQTm9jLVJBQTdXdnpkMGszbURITXRxNU9mMWNtV2lxMTFxUHZVSGNpLUM5eEVoaDVwV3BIT3pDV2x4TDRsOTB6Wnp4SUtobXJRbjkwU0FzZFZtMUdzSmlQZVVxNHR5Snp5VXFQWkN5UWdCeHZOQ1V2VDg1akZpdzI0SGxPLTNQOHRkNW9nLTFDYkpGZnZQUWN3V2ZtN2pJZXVnVE1fV1dKNDNOX1ZrY2FLOElvdlhRSDlkVm42YlppMWFfUzk5X2diQ2syWVRIVlJiWVZYNmZZY042aUw2Zmc5cldEWmxvcjl4Qi1zdFZ3TzZmNFpEV2w3UzBlbGJiTDhhUU1qclFGZmhreGJ3dVkxVVp6MDZ6eW16bl9MT1lGNVdTOGFpVW5fUzQ5ZF92LTdNWFY2RkNXR1JTdmZqQ3g3ZjNVdGdaUHozRFQwTzZ1WlBPOENYSVdPeXpkZWpkN0pTVzZEZUcwNzJwQUZRXzJWRTR4U05kQUppOTM5OEZKdUIyRmdiblNEaEpfQmxyUmw2d3RaMzc5a3poM3lHTERjdTdPaG9LRGluTEVnSklLUXpwS2lGblA2RE83MFFXVEdpMmVVajlpRy1XQWRPaFBUVSJ9","signature":"kXv0CIP3tXrEY5WiJi6VrzXm2EAlrE--SfskgfFlwoE"}},"protected":{"jwk":{"kty":"RSA","e":"AQAB","n":"rNXsDRVAOCWTJ5MYmF6jmQe4M6brQC1kB2rx6EVWwLGgPzwqd-HsdFsLTOoFnSn4CIgEs81x3-teBHU-VzvtL8cCaTvE0vLwz7LJpKGbquy8adBiIBFPBnm6ZAPKh4T4R49ZpA5yy6b40ZgPH1Be6NbutxSd0APVJSXr8emhJa88JFJIoKD6rDeQQMmNtKcgQBwzdJgK9VUdu8nGS2JLacdAQCsicXt_7PNoc-RAA7Wvzd0k3mDHMtq5Of1cmWiq11qPvUHci-C9xEhh5pWpHOzCWlxL4l90zZzxIKhmrQn90SAsdVm1GsJiPeUq4tyJzyUqPZCyQgBxvNCUvT85jFiw24HlO-3P8td5og-1CbJFfvPQcwWfm7jIeugTM_WWJ43N_VkcaK8IovXQH9dVn6bZi1a_S99_gbCk2YTHVRbYVX6fYcN6iL6fg9rWDZlor9xB-stVwO6f4ZDWl7S0elbbL8aQMjrQFfhkxbwuY1UZz06zymzn_LOYF5WS8aiUn_S49d_v-7MXV6FCWGRSvfjCx7f3UtgZPz3DT0O6uZPO8CXIWOyzdejd7JSW6DeG072pAFQ_2VE4xSNdAJi9398FJuB2FgbnSDhJ_BlrRl6wtZ379kzh3yGLDcu7OhoKDinLEgJIKQzpKiFnP6DO70QWTGi2eUj9iG-WAdOhPTU"},"url":"https:\/\/acme.zerossl.com\/v2\/DV90\/newAccount","alg":"RS256","nonce":"4M4Zycv7nLVzo1YX_CnhM7fEMIG40OJx3T0jckTnyV8"}}
2022/06/27 17:23:52 [debug] 30427#0: *802 [acme] client.lua:305: acme request: https://acme.zerossl.com/v2/DV90/newAccount response: {"type":"urn:ietf:params:acme:error:unauthorized","status":401,"detail":"The Request URL in the JWS Protected Header does not match the actual Request URL"}
2022/06/27 17:00:02 [error] 28754#0: *163 [kong] handler.lua:118 failed to update certificate: failed to create account: The Request URL in the JWS Protected Header does not match the actual Request URL, context: ngx.timer, client: 205.209.24.227, server: 0.0.0.0:443

Parsing the logs, you can see that the request is going to https://acme.zerossl.com/v2/DV90/newAccount but the information in the protected section of the payload has that the url is https:\/\/acme.zerossl.com\/v2\/DV90\/newAccount.

Is this an error with the jws encoding that is happening? Am I misunderstanding the role of the EAB credentials? What is the recommended approach to generated certificates throug ZeroSSL?

Thank you so much for your help!

Check cache from domain_whitelist_callback?

Hi folks,

Thank you for this awesome package! I have a quick question: is it possible to check for the existence in the domain certs in the cache from within the domain_whitelist_callback function?

The idea is that we'd query our domain whitelist endpoint only if certs can't be found in cache, to improve performance.

Since certs are cached for 1h, it means a domain previously whitelisted but no longer whitelisted will still get its certs served for 1h, but that's acceptable compared to the performance gain.

Renew failed missing lua_ssl_trusted_certificate in http{} block

Hello!
I am getting a certificate renewal error. I cannot understand what is the matter.
Thank you in advance.

`
2020/11/11 12:05:33 [error] 12#12: *47966 lua ssl certificate verify error: (20: unable to get local issuer certificate), context: ngx.timer

2020/11/11 12:05:33 [error] 12#12: *47966 [lua] autossl.lua:196: update_cert(): error during acme init: acme directory request failed: 20: unable to get local issuer certificate, context: ngx.timer

2020/11/11 12:05:34 [error] 12#12: *47966 lua ssl certificate verify error: (20: unable to get local issuer certificate), context: ngx.timer

2020/11/11 12:05:34 [error] 12#12: *47966 [lua] autossl.lua:196: update_cert(): error during acme init: acme directory request failed: 20: unable to get local issuer certificate, context: ngx.timer

`

should expose get_certkey method

first of all, awesome job. thanks for your kind work 👍 .

I need to check domain certkey exists or not, but I found this method only private.
It would be very convenient if this method can be exposed.

local autossl = require("resty.acme.autossl")
local certkey, err = autossl.get_certkey({domain=ngx.var.server_name, type="rsa"}) 
if certkey and scheme == http then
    ngx.redirect(new_uri, 301)
end

-- I had to hack autoss.lua to make this work

-- make get_certkey visible
AUTOSSL.get_certkey = get_certkey 

During renewal, errors on list() operation from consul storage is not logged: instead results in runtime error

During certificate renewal, a list() operation is done on the storage to get all the certificates. While it is rare that local storage might throw any error on the list operation, a storage like consul might have errors on this operation: for example here.

So what is happening right now when there is an error in the list operation is

  • consul storage returns nil, err as output
  • The function call in autossl.lua reads only the first output value into variable keys, which is nil
  • In the next line in the function, we get a runtime error, since keys is nil
  • In this process, the reason why the list() operation failed is lost

Instead what should happen is

  • consul storage returns nil, err
  • The function call in autossl.lua reads both the keys, and error message
  • It detects that error is not nil, logs the error and exits the function

can't get new nonce from acme server

Since some time, my certificates have stopped updating. I error log I see such error for all my domains:
[lua] autossl.lua:174: update_cert_handler(): error updating cert for domain.com err: failed to create new order: can't get new nonce from acme server, context: ngx.timer
I tried to get nonce by hand and it works:

# curl -k -I https://acme-v02.api.letsencrypt.org/acme/new-nonce
HTTP/2 200 
server: nginx
date: Wed, 12 May 2021 08:21:14 GMT
cache-control: public, max-age=0, no-cache
link: <https://acme-v02.api.letsencrypt.org/directory>;rel="index"
replay-nonce: 0103MSkULKV6LUWKI59T_afSuNeQRtcuI5Wp6NEtPRgZ-Bs
x-frame-options: DENY
strict-transport-security: max-age=604800

Also I found that from Jan 17 response code for HEAD request changed from 204 to 200 but as I see in client.lua you don't check the response code.

Not returning ERR correctly in update_cert_handler

Whilst working on #20 I noted that errors aren't being returned in update_cert_handler

if err then
log(ngx_ERR, "error updating cert for ", domain, " err: ", err)
return
end
local serialized = json.encode({
domain = domain,
pkey = pkey,
cert = cert,
type = typ,
updated = ngx.now(),
})
local err = AUTOSSL.storage:set(domain_cache_key, serialized)
if err then
log(ngx_ERR, "error storing cert and key to storage ", err)
return
end

The code is relying on that error for further down

err = update_cert_handler(data)
-- yes we don't release lock, but wait it to expire after negative cache is cleared
return err
end

Unless I'm missing something?

To match a pattern in multiple domain

how to set "domain_whitelist = { "domain1.com", "domain2.com", "domain3.com" },"

domain_whitelist = setmetatable({}, { __index = function(_, k)
return ngx.re.match(k, [[.example.domain1$] | [.domain2.com$]], "jo")
end}),

@fffonion

Storage adapter not connect redis

Hi, this my config redis

init_by_lua_block {
    require("resty.acme.autossl").init({
        tos_accepted = true,
        account_email = "[email protected]",
        api_uri = "https://acme.zerossl.com/v2/DV90",
        eab_kid = "key",
        eab_hmac_key = "key",
        challenge_start_delay = 0,
        storage_adapter = "redis",
        storage_config = {
            host = '127.0.0.1',
            port = 6379,
            database = 5,
            auth = 'password',
        },
    },{
        api_uri = "https://acme.zerossl.com/v2/DV90",  
    })
}

But it does not work

How to debug

Hey,

thanks for your work!

I'm trying to use this package but for some reason https:// connections aren't working. So I'm wondering how to best debug such an issue.

When using http:// all is fine:

curl --verbose http://subdomain.my-domain.com/test
*   Trying xxx.xxx.xxx.xxx...
* TCP_NODELAY set
* Connected to subdomain.my-domain.com (xxx.xxx.xxx.xxx) port 80 (#0)
> GET /test HTTP/1.1
> Host: subdomain.my-domain.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.19.3.1
< Date: Wed, 23 Dec 2020 22:41:17 GMT
< Content-Type: text/plain
< Content-Length: 11
< Connection: keep-alive
< Content-Type: text/plain
<
* Connection #0 to host subdomain.my-domain.com left intact
OK - test
* Closing connection 0

When using https:// I won't get any response – it will eventually close the connection with a t timeout:

curl --verbose https://subdomain.my-domain.com/test
*   Trying xxx.xxx.xxx.xxx...
* TCP_NODELAY set
* Connection failed
* connect to xxx.xxx.xxx.xxx port 443 failed: Operation timed out
* Failed to connect to subdomain.my-domain.com port 443: Operation timed out
* Closing connection 0
curl: (7) Failed to connect to subdomain.my-domain.com port 443: Operation timed out

Here's my nginx.conf file:

worker_processes 1;
error_log logs/error.log;
events {
  worker_connections 1024;
}
http {

  resolver 8.8.8.8 ipv6=off;

  lua_shared_dict acme 16m;

  # required to verify Let's Encrypt API
  lua_ssl_trusted_certificate /etc/ssl/certs/ca-bundle.trust.crt;
  lua_ssl_verify_depth 2;

  init_by_lua_block {
    require("resty.acme.autossl").init({
        -- setting the following to true
        -- implies that you read and accepted https://letsencrypt.org/repository/
        tos_accepted = true,
        account_key_path = "/home/ec2-user/openresty/certs/account.key",
        account_email = "[email protected]",
        domain_whitelist = { "my-domain.com", "subdomain.my-domain.com" },
    })
  }

  init_worker_by_lua_block {
      require("resty.acme.autossl").init_worker()
  }

  server {
    listen 80;
    listen 443 ssl;
    server_name subdomain.my-domain.com;
    # fallback certs, make sure to create them before hand
    ssl_certificate /home/ec2-user/openresty/certs/default.pem;
    ssl_certificate_key /home/ec2-user/openresty/certs/default.key;

    ssl_certificate_by_lua_block {
        require("resty.acme.autossl").ssl_certificate()
    }

    location /.well-known {
      content_by_lua_block {
        require("resty.acme.autossl").serve_http_challenge()
      }
    }

    location / {
      add_header Content-Type text/plain;
      return 200 'ok';
    }

    location /test {
      add_header Content-Type text/plain;
      return 200 'OK - test';
    }
  }
}

Problem with update locks

Hi,
we are occasionally seeing a problem with an update_lock not being released, which causes certificates not to be renewed until the lock is manually cleared.

2022/02/14 14:38:02 [info] 16#16: *37134811 [lua] autossl.lua:250: update_cert(): update is already running (lock key update_lock::www.somedomain.com exists), current type rsa, context: ngx.timer

Any idea what could lead to this?

Thank you very much!

Installing offline, from file system — instead of HTTP fetching things via opm?

Question:

( Now finally trying out lua-resty-acme :- )
8 months after my last topic: #5 "Differences from lua-resty-auto-ssl" )

The lua-resty-acme installation — it's basically just opm downloading files and placing in /usr/local/openresty/site/ typically?
But does not involve things like compiling C code?

Background: I'd like my OpenResty Docker image to build offline, including installing lua-resty-acme without any Internet access.

However, lua-resty-acme is installed via opm which, it seems to me, wants to download the package plus dependencies, from a server somewhere (default opm.openresty.org, right).

I found a "workaround" — and I wonder what do you think about it? Would you expect lua-resty-acme to function properly, after having gotten installed as described below? (For example, if opm compiles any C code as part of installing lua-resty-acme, then, that wouldn't happen )

  1. From inside the Docker image (launched as a container), I ran tree to get a list of the files in the Docker image.

  2. Then, installed: opm install fffonion/lua-resty-acme

  3. I ran tree again. And then diffed files after, with files before. Apparently opm installed lua-resty-acme and dependencies in /usr/local/openresty/site.

  4. I copied that site/ dir from the container filesystem, to the host filesystem.

  5. Now in the Dockerfile, I just COPY that site/ dir into the Docker image, instead of runnig opm install .... — So, no network access needed.

I've tried this a little bit, seems to work fine — but I'm thinking it can be good to ask if you can think of problems with this approach?

(To upgrade, I'd just run opm install ... again, and repeat the steps above.)

( Sorry if maybe this is off-topic. Maybe could also ask the opm people. However, I'm thinking this is a bit specific to lua-resty-acme in that this depends on what installation steps it does and doesn't.)

Add hook or configurable cool off when generation fails

Would it be possible to add a hook or something along those lines for a user supplied function for when certification generation fails.

The problem I'm trying to solve is that the application that sits behind openresty is configurable to have any user supplied domain. We then gather those domains and add them to the whitelist used by resty acme.

However there are times that those domains either aren't publicly available or resolvable.

My thoughts are that if we had a user configurable function that could be used on certification creation failure user supplied code could then handle these situations, or adding a configurable cool down period so that acme doesn't keep trying to generate a failed certificate.

attempt to concatenate local 'err' (a boolean value)

Installed lua-resty-acme using opm, configuring it according to the readme, getting the following errors:

2020/02/09 18:08:21 [error] 23589#23589: *12 lua entry thread aborted: runtime error: /usr/local/openresty/site/lualib/resty/acme/util.lua:63: attempt to concatenate local 'err' (a boolean value)
stack traceback:
coroutine 0:
	/usr/local/openresty/site/lualib/resty/acme/util.lua: in function 'create_csr'
	/usr/local/openresty/site/lualib/resty/acme/client.lua:423: in function 'order_certificate'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:169: in function 'update_cert_handler'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:219: in function 'update_cert'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:388: in function </usr/local/openresty/site/lualib/resty/acme/autossl.lua:385>, context: ngx.timer, client: 1.1.1.1, server: 0.0.0.0:443
2020/02/09 18:43:22 [error] 23802#23802: *12 lua entry thread aborted: runtime error: /usr/local/openresty/site/lualib/resty/acme/util.lua:63: attempt to concatenate local 'err' (a boolean value)
stack traceback:
coroutine 0:
	/usr/local/openresty/site/lualib/resty/acme/util.lua: in function 'create_csr'
	/usr/local/openresty/site/lualib/resty/acme/client.lua:423: in function 'order_certificate'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:169: in function 'update_cert_handler'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:219: in function 'update_cert'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:388: in function </usr/local/openresty/site/lualib/resty/acme/autossl.lua:385>, context: ngx.timer, client: 1.1.1.1, server: 0.0.0.0:443
2020/02/09 18:45:08 [error] 23802#23802: *10 lua entry thread aborted: runtime error: ...l/openresty/site/lualib/resty/acme/challenge/http-01.lua:37: attempt to index local 'captures' (a nil value)
stack traceback:
coroutine 0:
	...l/openresty/site/lualib/resty/acme/challenge/http-01.lua: in function 'serve_challenge'
	/usr/local/openresty/site/lualib/resty/acme/client.lua:446: in function 'serve_http_challenge'
	/usr/local/openresty/site/lualib/resty/acme/autossl.lua:342: in function 'serve_http_challenge'
	content_by_lua(myserver.tld.conf:30):2: in main chunk, client: 1.1.1.1, server: myserver.tld, request: "GET /.well-known HTTP/2.0", host: "myserver.tld"

May this be a misconfiguration or a bug?

BoringSSL not working HTTP3

Hi how could this error be resolved?

journalctl -xe [log]

nginx: [error] init_by_lua error: /usr/local/share/lua/5.1/resty/openssl/version.lua:60: OpenSSL has encountered an error: /usr/local/share/lua/5.1/resty/openssl/version.lua:45: /usr/local/lib/libluajit-5.1.so.2: undefined symbol: SSLeay; is OpenSSL library loaded?

Aug 18 15:44:15 localserver nginx[71449]: stack traceback:
Aug 18 15:44:15 localserver nginx[71449]:         [C]: in function 'error'
Aug 18 15:44:15 localserver nginx[71449]:         /usr/local/share/lua/5.1/resty/openssl/version.lua:60: in main chunk
Aug 18 15:44:15 localserver nginx[71449]:         [C]: in function 'require'
Aug 18 15:44:15 localserver nginx[71449]:         /usr/local/share/lua/5.1/resty/acme/openssl.lua:4: in main chunk
Aug 18 15:44:15 localserver nginx[71449]:         [C]: in function 'require'
Aug 18 15:44:15 localserver nginx[71449]:         /usr/local/share/lua/5.1/resty/acme/util.lua:4: in main chunk
Aug 18 15:44:15 localserver nginx[71449]:         [C]: in function 'require'
Aug 18 15:44:15 localserver nginx[71449]:         /usr/local/share/lua/5.1/resty/acme/client.lua:3: in main chunk
Aug 18 15:44:15 localserver nginx[71449]:         [C]: in function 'require'
Aug 18 15:44:15 localserver nginx[71449]:         /usr/local/share/lua/5.1/resty/acme/autossl.lua:2: in main chunk
Aug 18 15:44:15 localserver nginx[71449]:         [C]: in function 'require'
Aug 18 15:44:15 localserver nginx[71449]:         init_by_lua:3: in main chunk
Aug 18 15:44:15 localserver systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE

nginx -V

root@localserver:/etc/nginx# nginx -V
nginx version: nginx/1.19.6
built with OpenSSL 1.1.1 (compatible; BoringSSL) (running with BoringSSL)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --with-openssl=quiche/quiche/deps/boringssl --with-quiche=quiche --with-http_ssl_module --with-http_v2_module --with-http_v3_module --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --with-pcre --lock-path=/var/lock/nginx.lock --pid-path=/var/run/nginx.pid --with-http_ssl_module --with-http_sub_module --with-http_image_filter_module=dynamic --modules-path=/etc/nginx/modules --add-module=modules/ngx_devel_kit-0.3.1 --add-module=modules/ngx_pagespeed --add-module=modules/ngx_brotli --add-module=modules/headers-more-nginx-module-0.33 --add-module=modules/lua-nginx-module-0.10.21 --with-cc-opt='-I quiche/quiche/deps/boringssl/include' --with-ld-opt='-L quiche/quiche/deps/boringssl/build/ssl -L quiche/quiche/deps/boringssl/build/crypto'

get_certkey_parsed: Pass stale certificate if getting certificate from storage fails

If file-system is used as storage for cert, the only reason why getting certificate from storage fails is because the certificate does not exist in the first place. If it is a storage system like vault, it can fail due to networking errors, timeout due to vault being busy etc. Not serving a certificate when this happens would result in HTTPS connection errors.

So it makes sense that we provide stale certificate from cache, if it is present, when the request to get certificate from storage fails. Just logging errors would be enough. This way we can improve the reliability of the HTTPS requests.

Account key and domain keys written as files regardless of storage backend?

Is this deliberate? It seems like an odd mismatch to me. My nginx is a Kubernetes pod so the local storage may disappear. My etcd storage, however, is persistent. And yes, my colleague is writing an etcd backend for this project. 😄

Perhaps you can convince me why files are more appropriate here. Otherwise would you accept a patch to change this behaviour? Or perhaps make it configurable?

While I'm here, I'm changing api_uri to point to my test ACME server but it's still trying to use the real one. Haven't figured out why yet.

We're working on this as a company hackathon project over the next day so a speedy response would be appreciated.

Zerossl config api_uri but error ngx.timer

Hi, I config for openresty, but when I request domain Nginx log error

2022/08/02 02:17:58 [error] 12907#12907: *35 [acme] autossl.lua:224: error during acme init: acme directory request failed: 20: unable to get local issuer certificate, context: ngx.timer, client: 14.161.31.54, server: 0.0.0.0:443

2022/08/02 02:17:58 [error] 12907#12907: *39 lua ssl certificate verify error: (20: unable to get local issuer certificate), context: ngx.timer, client: 14.161.31.54, server: 0.0.0.0:443

This is my config

require("resty.acme.autossl").init({
        tos_accepted = true,
        account_email = "",
        api_uri = "https://acme.zerossl.com/v2/DV90",
        eab_kid = "",
        eab_hmac_key = "",
})

How to fix it.

Thank you so much for your help!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.