{"id":1436,"date":"2022-06-18T19:22:48","date_gmt":"2022-06-18T19:22:48","guid":{"rendered":"https:\/\/brakkee.org\/site\/?p=1436"},"modified":"2022-08-14T08:57:06","modified_gmt":"2022-08-14T08:57:06","slug":"automatic-certificate-renewal-with-lets-encrypt-and-dnsmadeeasy-on-kubernetes","status":"publish","type":"post","link":"https:\/\/brakkee.org\/site\/2022\/06\/18\/automatic-certificate-renewal-with-lets-encrypt-and-dnsmadeeasy-on-kubernetes\/","title":{"rendered":"Automatic certificate renewal with Let&#8217;s Encrypt and DnsMadeEasy on Kubernetes"},"content":{"rendered":"<p>These days, it is ill-advised to run a website (such as this one), over HTTP, even if there is no security risk at all. When hosting your website on HTTP, users will see a warning triangle in the address bar and many users will simply turn away. This is especially painful if the website is blogging on devops and security related things.<\/p>\n<p>Currently, I am migrating everything I have locally to a more secure and future proof setup from VMs to kubernetes. The approach I decided to take is to start with the front-facing services and move everything from there step by step. Therefore, the first step is to move the reverse proxy from a VM to kubernetes. Since I don&#8217;t want to pay for certificates, I will be using <a href=\"https:\/\/letsencrypt.org\/\">Let&#8217;s Encrypt<\/a>. Unfortunately, Let&#8217;s Encrypt only supports 90 day certificates so this means a lot of certificate renewals.<\/p>\n<p><!--more--><\/p>\n<h2>The ACME protocol<\/h2>\n<p>To support this Let&#8217;s Encrypt supports the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Automatic_Certificate_Management_Environment\">ACME protocol<\/a>, which allows for automatic certificate renewal. The ACME protocol automates the complete process. A certificate renewal process looks like this:<\/p>\n<ul>\n<li>create a new private key<\/li>\n<li>create a certificate signing request using the private key<\/li>\n<li>request a certificate from the CA (Certificate Authority)<\/li>\n<li>the CA asks for verification of ownership of the domain either using a HTTP challenge for a single domain or a DNS challenge for a wildcard domain. The HTTP challenges amounts to putting a certain file with the request content (the challenge) in the URL space of the domain. The DNS challenge typically asks for a TXT record to be created in the DNS for the domain with the requested content. If someone is able to do that then ownership is confirmed<\/li>\n<li>confirm to the HTTP or DNS challenge by creating the appropriate file or DNS record respectively<\/li>\n<li>notify the CA that the challenge was answered<\/li>\n<li>the CA checks the challenge<\/li>\n<li>the CA issues the certificate after the challenge is verified.<\/li>\n<\/ul>\n<h2>Automatic renewal: cert-manager<\/h2>\n<p>There are various tools for automatically certificates updates based on the ACME protocol, but most of them are for VMs and have options for directly updating apache of nginx configuration files. Imagine what kind of horror this could be if such a script would corrupt your special\/home-grown configuration. On kubernetes things are a lot more clean using <a href=\"https:\/\/cert-manager.io\/\">cert-manager<\/a>.<\/p>\n<p>Cert-manager introduces a number of concepts:<\/p>\n<ul>\n<li><strong>Certificate<\/strong>: A custom resource describing a certificate. This defines the names (DNS alternative names) that will appear on the certificate as well as the name of the TLS secret that will be created and which is used by ingress.<\/li>\n<li><strong>Issuer<\/strong>: The object that is responsible for using the ACME protocol to contact Let&#8217;s Encrypt. It uses a webhook for the specific DNS provider. The task of the web hook is to create and remove the challenge records as required by Let&#8217;s Encrypt. In my setup I am using the DNS challenge since I want to get wildcard certificates.<\/li>\n<li><strong>Webhook<\/strong>: A custom webhook that is used by an issuer to comply to the challenges from the CA.<\/li>\n<\/ul>\n<h2>Installation<\/h2>\n<p>Install cert-manager by following the instructions on the website. In my case, I did the following:<\/p>\n<pre>helm repo add jetstack https:\/\/charts.jetstack.io\nhelm repo updatehelm install \\\n  cert-manager jetstack\/cert-manager \\\n    --namespace cert-manager \\\n    --create-namespace \\\n    --version v1.8.0 \\\n    --set installCRDs=true<\/pre>\n<p>After this, I used <em>cmctl<\/em> to verify the installation<\/p>\n<pre>OS=$(go env GOOS); ARCH=$(go env GOARCH); curl -sSL -o cmctl.tar.gz https:\/\/github.com\/cert-manager\/cert-manager\/releases\/download\/v1.7.2\/cmctl-$OS\n-$ARCH.tar.gz\ntar xvf cmctl.tar.gz\ncmctl check api<\/pre>\n<p>The cert-manager website contains\u00a0 more instructions to verify the installation using a self-signed certificate. It is recommended to follow these instructions to make sure that everything works.<\/p>\n<p>Next up is the installation of the DNS madeeasy webhook.<\/p>\n<pre>helm repo add k8s-at-home https:\/\/k8s-at-home.com\/charts\/\nhelm repo updatehelm install dnschallenger k8s-at-home\/dnsmadeeasy-webhook --namespace cert-manager -f values-dnsmadeeasychallenger.yaml \n\n<\/pre>\n<p>The <em>values-dnsmadeeasychallenger.yaml<\/em> configuration file is used to define the <em>groupName<\/em> which is used by the issuer to identify the webhook:<\/p>\n<p><em>values-dnsmadeeaychallenger.yaml<\/em><\/p>\n<pre># This name will need to be referenced in each Issuer's `webhook` stanza to\n# inform cert-manager of where to send ChallengePayload resources in order to\n# solve the DNS01 challenge.\n# This group name should be **unique**, hence using your own company's domain\n# here is recommended.\ngroupName: dnsmadeeasy.challenger<\/pre>\n<p>Note that I installed the webhook in the same namespace as cert-manager since it is an integral part of certificate management and belongs in the same namespace.<\/p>\n<h2>Configuration<\/h2>\n<p>We need to define some essential information for everything to work:<\/p>\n<ul>\n<li>the DNS alternative names of the certificate. These are configured in the <em>Certificate<\/em> resource.<\/li>\n<li>the name of the TLS secret that will contain the generated certificates. This is configured in the <em>Certificate<\/em> resource.<\/li>\n<li>the API key and secret to be able to use the DnsMadeEasy API. This is a separate <em>Secret<\/em> resource.<\/li>\n<\/ul>\n<p>Examples for <em>brakkee.org<\/em> are as follows:<\/p>\n<p><em>brakkee-org-certificate.yaml<\/em><\/p>\n<pre>apiVersion: cert-manager.io\/v1\nkind: Certificate\nmetadata:\n  name: wildcard-brakkee-org\n  namespace: exposure\nspec:\n  dnsNames:\n  - \"*.brakkee.org\"\n  - \"brakkee.org\"\n  issuerRef:\n    name: dnsmadeeasy-issuer\n    kind: Issuer\n  secretName: brakkee-org-tls-secret\n<\/pre>\n<p>Note that I am defining the root domain <em>brakkee.org<\/em> as a separate name. This is because the wildcard comain <em>*.brakkee.org<\/em> only covers subdomains of <em>brakkee.org<\/em> but not the root domain <em>brakkee.org<\/em>.<\/p>\n<p><em>dnsmadeeasy-apikey.yaml<\/em><\/p>\n<pre>apiVersion: v1\nkind: Secret\nmetadata:\n  name: dnsmadeeasy-apikey\n  namespace: cert-manager\ntype: Opaque\nstringData:\n  key: your_key_here\n  secret: your_secret_here\n\n<\/pre>\n<p>Apart from this, we need to configure the issuer to use the webhook and to pass the DnsMadeEasy API key and secret on to the webhook to comply to the challenge:<\/p>\n<p><em>dnsmadeeasy-issuer.yaml<\/em><\/p>\n<pre>apiVersion: cert-manager.io\/v1\nkind: Issuer\nmetadata:\n  name: dnsmadeeasy-issuer\n  namespace: exposure\nspec:\n  acme:\n    #server: https:\/\/acme-staging-v02.api.letsencrypt.org\/directory\n    server: https:\/\/acme-v02.api.letsencrypt.org\/directory\n    email: info@brakkee.org\n    privateKeySecretRef:\n      name: hosting-key-secret\n    solvers:\n    - dns01:\n        webhook:\n          groupName: dnsmadeeasy.challenger\n          solverName: dnsmadeeasy\n          config:\n            apiKeyRef:\n              name: dnsmadeeasy-apikey\n              key: key\n            apiSecretRef:\n              name: dnsmadeeasy-apikey\n              key: secret\n\n<\/pre>\n<p>In the above file, note the server attribute where you can use the staging URL for Let&#8217;s Encrypt if you want to test. This is useful because of the strict rate limits on the Let&#8217;s Encrypt API. Note the <em>groupName<\/em>\u00a0 which links back to the webhook we installed earlier. Also note the reference to the API key sedret to pass on configuration values to the web hook. The private key is stored in a separate secret (hosting-key-secret). This secret is created by the Issuer.<\/p>\n<p>After applying the above resources, first a temporary secret will be created in the exposure namespace. After it is finished, you should have a new TLS secret brakkee-org-tls-secret in the exposure namespace that can be used in an ingress rule. During the process you can do a <em>kubectl describe<\/em> on the certificate resource to see the progress.<\/p>\n<p>Note that I am creating all these resources in a separate <em>exposure<\/em> namespace. This is the namespace where I will have all SSL termination using ingress. Backend applications will typically be running in different namespaces. This will turn out to be important in a future post where I will go into setting up network policies to improve security.<\/p>\n<h2>Using the TLS secret in an ingress rule<\/h2>\n<p>Using the TLS secret in an ingress rule is straightforward. In my case I am forwarding all traffic for <em>brakkee.org<\/em> and <em>*.brakkee.org<\/em> to the same HTTPD backend service. For example:<\/p>\n<pre>apiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: ingress-brakkee-org\n  namespace: exposure\nspec: \n  tls: \n  - hosts:\n    - \"*.brakkee.org\"\n    - \"brakkee.org\"\n    secretName: brakkee-org-tls-secret\n  rules:\n  - host: \"brakkee.org\"\n    http: &amp;proxy_rules\n      paths:\n      - path: \/\n        pathType: Prefix \n        backend:\n          service:\n            name: httpd-reverse-proxy-brakkee-org\n            port: \n              number: 80\n  - host: \"*.brakkee.org\"\n    http: *proxy_rules\n\n<\/pre>\n<p>A nice trick here is to use a YAML anchor (&amp;proxy_rules). This allows me to avoid duplicating the same rules for <em>brakkee.org<\/em> and <em>*.brakkee.org<\/em>. The backend service <em>httpd-reverse-proxy-brakkee-org<\/em> is not shown in this post but it is not too hard to adapt the example to use your own backend service. It is easy to add more domains with their own certificates simply by adding <em>hosts<\/em> entries to the tls section and by adding rules.<\/p>\n<h2>Certificate renewal<\/h2>\n<p>I have tested certificate renewal by using the spec.renewBefore field in the Certificate to force earlier renewal. For example:<\/p>\n<pre>apiVersion: cert-manager.io\/v1\nkind: Certificate\nmetadata:\n  name: wildcard-brakkee-org\n  namespace: exposure\nspec:\n  renewBefore: 2136h\n  ...<\/pre>\n<p>It is not possible to request a shorter duration of the certificate since Let&#8217;s Encrypt always generates 90 day certificates. Just choose renewBefore counting back from the 90 day expiry time of the certificate.<\/p>\n<h2>Force regeneration of the certificate<\/h2>\n<p>To force regeneration of the secret you can just delete the generated secret and it will get generated again. The same applies to the host key secret.<\/p>\n<h2>Rate limits<\/h2>\n<p>If you run into a rate limit at Let&#8217;s Encrypt, note that the rate limit applies to the list of requested DNS alternative names (<em>*.brakkee.org<\/em>, <em>brakkee.org<\/em> in my case). The current rate limit is 5 times per week for each unique combination of DNS alternative names. If you run into this limit, then temporarily add another DNS alternative name such as\u00a0<em>x.y.brakkee.org<\/em> to the list. This name does not fall under <em>*.brakkee.org<\/em>. Using this it is easy to work around the limit. It is of course always better to use the staging URL of Let&#8217;s Encrypt before you start using tricks.<\/p>\n<h2>Costs<\/h2>\n<p>To use the DnsMadeEasy APIs I had to upgrade my membership to a business membership which costs 75 USD per year. But at least I am not manually updating certificates and not paying a similar amount per domain.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>These days, it is ill-advised to run a website (such as this one), over HTTP, even if there is no security risk at all. When hosting your website on HTTP, users will see a warning triangle in the address bar &hellip; <a href=\"https:\/\/brakkee.org\/site\/2022\/06\/18\/automatic-certificate-renewal-with-lets-encrypt-and-dnsmadeeasy-on-kubernetes\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[10],"tags":[],"_links":{"self":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts\/1436"}],"collection":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/comments?post=1436"}],"version-history":[{"count":49,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts\/1436\/revisions"}],"predecessor-version":[{"id":1735,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/posts\/1436\/revisions\/1735"}],"wp:attachment":[{"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/media?parent=1436"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/categories?post=1436"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/brakkee.org\/site\/wp-json\/wp\/v2\/tags?post=1436"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}