Git Product home page Git Product logo

kubernetes-letsencrypt's Introduction

Kubernetes Letsencrypt Controller

Build Status

This implements a Kubernetes controller that automatically requests and refreshes Letsencrypt certificates based on service annotations.

This controller currently supports Amazon Route 53 and Google Cloud DNS as the DNS targets.

Setup

Launch the controller into your cluster using

kubectl apply -f letsencrypt-controller.yaml

This will use a release or snapshot version (depending on your git checkout) hosted on my Docker Hub account.

The pod must run with the permissions required for updating records in the DNS zones that you maintain.

On AWS, consider using a project such as kube2iam to grant permissions to individual pods.

Please refer to the 'Building' section for using your own image.

Configuration

The controller currently supports three configuration options via environment variables:

  • ACME_URL: This can be set to an alternative ACME directory URL, for example the Letsencrypt staging server if you only want to test out the controller.
  • CLOUD_PLATFORM: This can be set to either GCP or AWS to override the automatic platform detection. You can use this to for example use Route53 as the DNS backend with a cluster running on Google's Cloud Platform. If you override this option you must provide credentials for the DNS backend, for example via the environment variables for the Google Cloud Java SDK or the AWS Java SDK
  • LOG_LEVEL: This can be used to set the log level to something other than the default (INFO).

Usage

Simply add an annotation to your services, for example:

---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
  annotations:
    acme/certificate: www.yourdomain.com
spec:
  type: LoadBalancer
[...]

The controller will notice this and, assuming you have a matching hosted zone, create a certificate and store it as a secret named www-yourdomain-com-tls.

You can override the name of the secret by specifying an annotation called acme/secretName.

You may specify multiple domains to include in a certificate as a JSON array. This requires setting the acme/secretName annotation. For example:

[...]
metadata:
  annotations:
    acme/certificate: '["yourdomain.com", "www.yourdomain.com"]'
    acme/secretName: mydomain-certificate
[...]

The certificate secret will contain four files named certificate.pem, chain.pem, key.pem and fullchain.pem. You can mount these into whatever application you use to terminate TLS.

If required, you can configure these file names via the environment variables, CERTIFICATE_FILENAME, CHAIN_FILENAME, KEY_FILENAME, FULLCHAIN_FILENAME.

The secret will always be created in the same namespace as your service. Removing the annotation will never remove a secret.

Certificate renewals

Every secret will be annotated with the certificate expiry date. The controller will refresh the certificate and update the secret once the expiry date is close.

Currently this update happens within 1-2 days of expiry. The reason for the short time-interval is that Letsencrypt has a long-term desire to reduce the certificate lifespans so I am trying to be future-proof here.

Overview

The controller first attempts to find a secret in the Kubernetes kube-system namespace with the name letsencrypt-keypair. This secret is expected to contain the key pair used for authentication with the Letsencrypt service.

If no such key pair is found the controller will create one and store it as a secret.

On startup the controller will check all existing services for an annotation

Building

All build lifecycle steps are handled in Gradle. After determining your desired image name, you can build a new image with:

# Run test suite
./gradlew test

# Create local Docker image
./gradlew dockerBuildImage

This will build an image locally with the tag tazjin/letsencrypt-controller:${VERSION}, where ${VERSION} is the one specified in build.gradle.kts.

Contributing

Feel free to contribute pull requests, file bugs and open issues with feature suggestions!

Please follow the code of conduct.

kubernetes-letsencrypt's People

Contributors

cedricgc avatar ensonic avatar itomaldonado avatar tazjin 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

kubernetes-letsencrypt's Issues

NullPointerException in DnsRecordObserver.findAuthoritativeNameservers

If the DNS root record hasn't propagated yet, the DnsRecordObserver will crash. This is a minor issue, as the logs suggest it retries (although as mentioned in #72, exponential backoff would reduce the load on the DNS servers).

lookup.run returns null when no records are found, so perhaps this case could be logged as "No records for {root} found - retrying".

Exception in thread "Thread-3" java.lang.NullPointerException
	at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:267)
	at in.tazj.k8s.letsencrypt.util.DnsRecordObserver.findAuthoritativeNameservers(DnsRecordObserver.kt:76)
	at in.tazj.k8s.letsencrypt.util.DnsRecordObserver.observeDns(DnsRecordObserver.kt:24)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.prepareDnsChallenge(CertificateRequestHandler.kt:179)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.authorizeDomain(CertificateRequestHandler.kt:77)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.access$authorizeDomain(CertificateRequestHandler.kt:27)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:27)
	[SNIP: java.util.*]
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.requestCertificate(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.handleCertificateRequest(ServiceManager.kt:64)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.access$handleCertificateRequest(ServiceManager.kt:20)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager$reconcileService$1.run(ServiceManager.kt:45)
	at java.lang.Thread.run(Thread.java:745)

Deal with license agreement updates

Currently we accept license agreements from Letsencrypt on user registration and inform the user about this. We must deal with updates to the license agreements though, because they will currently break the controller.

Use Kubernetes custom resource definitions

Kubernetes has support for third-party resources.

Ideally instead of using service annotations this controller would watch a custom third-party CertificateRequest object and create secrets based on that to decouple consuming services from the certificates and to make certificates cluster-native resources.

Currently the Java SDK for Kubernetes developed by Fabric8 does not support third-party resources. This issue is tracked in fabric8io/kubernetes-client#299

Set up tests & CI

Title says it all.

Current snapshot work should be deployed to Docker Hub automatically as well.

Support different namespaces

Currently all resources are monitored and created in the default namespace. Ideally a ServiceWatcher would be set up for every namespace and it would be aware of the namespace it is watching in order to create Secrets in the correct places.

Consider also managing the DNS Records

Hi, I just tried out this project in our cluster, and it worked flawlessly. The only thing I feel is missing was that kubernetes-letsencrypt could also provision the actual DNS record.

It seems the project has everything that is needed, as it already creates the _acme-challenge DNS record, so I hope it wouldn't be too tricky.

Detecting Google Cloud DNS zone

Hi,

I think this is more likely something I don't quite follow with the auth/challenge flow, but here is the behaviour we're seeing in Google Cloud.

  • I create a service with acme/certificate: lb.flags0.gcp0.example.net.
  • The controller creates a TXT record at _acme-challenge.lb.flags0.gcp0.example.net..
  • letsencrypt fails to find the above record, because it is querying for _acme-challenge.gcp0.example.net.

I've worked around this by copying the digest from the lb.flags0 TXT record into the zone at gcp0.example.net (which is a different GCP project), but I presume there is something going wrong in the flow here.

Google Cloud DNS challenges fail sometimes

As mentioned in 4e3bbd6 and the comment in the code, Cloud DNS updates sometimes have not fully propagated when they are marked as "DONE" and even when the DNS observer sees the change in all nameservers.

Presumably this is some eventual consistency deal on Google's side. It is "solved" for now with an artificial wait timer, but long-term we should figure out what causes it, if there's documentation about it and how to deal with it better.

Allow single certificate for multiple services

We're starting to hit letsencrypt rate limits for number of subdomains on a registered domain (20 certs a week).

This could be mitigated by bundling up each service's domains into a request for a single certificate for each registered domain, and then copying that out across multiple secrets.

Admittedly we could work around this problem by adding all the subdomains to a single (dummy) service's acme/certificate: list (and having a single shared secret), but we very much like the semantics of each service being self-contained.

Do you think this would be something that could be handled easily in the current architecture, and would you be open to pull requests to support such a feature?

Key values mismatch in nginx

Happened with one certificate, retrieving it again apparently fixed the bug:

2016/09/21 09:12:14 [emerg] 1#1: SSL_CTX_use_PrivateKey_file("/etc/nginx/tls/key.pem") failed (SSL: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch)
nginx: [emerg] SSL_CTX_use_PrivateKey_file("/etc/nginx/tls/key.pem") failed (SSL: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch)

A similar issue was discussed on Let's Encrypt forums.

Maybe there is something the controller can do after retrieving the certificates to ensure that the key and cert match?

GCloud DNS updates failing

Since merging #47 GCloud updates fail with:

Exception in thread "Thread-77" com.google.cloud.dns.DnsException: Invalid value for 'entity.change.additions[0].rrdata': '<empty>'
	at com.google.cloud.dns.spi.DefaultDnsRpc.translate(DefaultDnsRpc.java:183)
	at com.google.cloud.dns.spi.DefaultDnsRpc.applyChangeRequest(DefaultDnsRpc.java:316)
	at com.google.cloud.dns.DnsImpl$9.call(DnsImpl.java:301)
	at com.google.cloud.dns.DnsImpl$9.call(DnsImpl.java:298)
	at com.google.cloud.RetryHelper.doRetry(RetryHelper.java:179)
	at com.google.cloud.RetryHelper.runWithRetries(RetryHelper.java:244)
	at com.google.cloud.dns.DnsImpl.applyChangeRequest(DnsImpl.java:297)
	at com.google.cloud.dns.Zone.applyChangeRequest(Zone.java:178)
	at in.tazj.k8s.letsencrypt.acme.CloudDnsResponder.updateCloudDnsRecord(CloudDnsResponder.java:109)
	at in.tazj.k8s.letsencrypt.acme.CloudDnsResponder.removeChallengeRecord(CloudDnsResponder.java:49)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.lambda$prepareDnsChallenge$1(CertificateRequestHandler.java:187)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.authorizeDomain(CertificateRequestHandler.java:85)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.lambda$requestCertificate$0(CertificateRequestHandler.java:59)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
	at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:401)
	at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:734)
	at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:583)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.requestCertificate(CertificateRequestHandler.java:59)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.handleCertificateRequest(ServiceManager.java:108)
	at java.util.Optional.ifPresent(Optional.java:159)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.lambda$reconcileService$0(ServiceManager.java:58)
	at java.lang.Thread.run(Thread.java:745)
Caused by: com.google.api.client.googleapis.json.GoogleJsonResponseException: 400 Bad Request
{
  "code" : 400,
  "errors" : [ {
    "domain" : "global",
    "message" : "Invalid value for 'entity.change.additions[0].rrdata': '<empty>'",
    "reason" : "invalid"
  } ],
  "message" : "Invalid value for 'entity.change.additions[0].rrdata': '<empty>'"
}
	at com.google.api.client.googleapis.json.GoogleJsonResponseException.from(GoogleJsonResponseException.java:145)
	at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:113)
	at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:40)
	at com.google.api.client.googleapis.services.AbstractGoogleClientRequest$1.interceptResponse(AbstractGoogleClientRequest.java:321)
	at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1056)
	at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:419)
	at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:352)
	at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:469)
	at com.google.cloud.dns.spi.DefaultDnsRpc.applyChangeRequest(DefaultDnsRpc.java:314)
	... 29 more

This is only for the snapshot builds, not stable.

Error creating new authz :: too many currently pending authorizations

Using kubernetes-letsencrypt v1.7 with Cloud DNS and GKE, we've observed a "too many currently pending authorizations" error. This is surprising, since the limit is 300 pending authorizations, but we only have ~10 certificates on the domain. kubernetes-letsencrypt was previously working fine, but when a new team member tried to bring up their own cluster, they ran into this issue.

On the Let's Encrypt forums, schoen said:

So I think the likeliest interpretation is [...] it sometimes request an authorization and then not use it (either requesting an authorization when not requesting a certificate, or requesting an authorization and then crashing or exiting before the corresponding certificate can be requested). This could, for example, be a renewal-related bug if one part of the code says "this certificate should be renewed now" but another part of the code says "this certificate is not yet due for renewal".

and

Maybe this does lead to some useful guidance for client developers: if you get an authz for one requested domain but fail to get it for another, make sure you proactively destroy the first authz before giving up. (If your error was based on repeated failed attempts to get a certificate for a mixture of names you do and don't control, that might be the underlying problem here.)

Is that possible? If we see it again, what can we do to get more debug information?

org.shredzone.acme4j.exception.AcmeRateLimitExceededException: Error creating new authz :: too many currently pending authorizations
        at org.shredzone.acme4j.connector.DefaultConnection.createAcmeException(DefaultConnection.java:394)
        at org.shredzone.acme4j.connector.DefaultConnection.accept(DefaultConnection.java:199)
        at org.shredzone.acme4j.Registration.authorizeDomain(Registration.java:189)
        at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.getAuthorization(CertificateRequestHandler.kt:90)
        at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.authorizeDomain(CertificateRequestHandler.kt:68)
        at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.access$authorizeDomain(CertificateRequestHandler.kt:27)
        at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:41)
        at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:27)
        at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
        at java.util.Collections$2.tryAdvance(Collections.java:4717)
        at java.util.Collections$2.forEachRemaining(Collections.java:4725)
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
        at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
        at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
        at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
        at java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:401)
        at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:734)
        at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
        at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174)
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
        at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
        at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:583)
        at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.requestCertificate(CertificateRequestHandler.kt:41)
        at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.handleCertificateRequest(ServiceManager.kt:64)
        at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.access$handleCertificateRequest(ServiceManager.kt:20)
        at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager$reconcileService$1.run(ServiceManager.kt:45)
        at java.lang.Thread.run(Thread.java:745)

Skip re-authorization of already confirmed domains

Let's Encrypt authorizations stay valid for some time (bound to the account registration via the keypair we store in the cluster).

It is possible to re-use existing authorisations by checking their status. If an authorisation status is still VALID we do not need to perform a new challenge.

Detect cloud platform automatically

Once #3 is implemented we should have some way to detect the cloud platform automatically.

Maybe it is possible to retrieve this information from the Kubernetes master?

Route 53 Split-horizon DNS

When using split-horizon DNS in AWS we have two hosted zones (a private and a public one) with the same domain.

Right now the service is choosing the first one it finds (which is the private one) and LE verification times out.

Since for LE to verify your cert it needs to do a public DNS lookup, this service should default to the Public hosted-zone if it finds more than one zone with the same domain.

Support SAN certificates

First idea:

Let users put JSON arrays into annotations, e.g.:

acme/certificate: '["tazj.in", "www.tazj.in"]'

Once #2 is fixed this should be a separate field in the resource!

Implementation wise:

  1. Challenge handling has to be split out & done for each domain with the same Authorization.
  2. We can probably do 1 in a parallelStream()

Q:

  1. Certificate naming becomes unclear! This ties into #17

prepareDnsChallenge cleanup exception

May be related (or not) to #61 but I am getting this error when it tries clean up the DNS challenge. I built the docker container myself (using your gradlew commands)so I am not sure if it something I did.

{"@timestamp":"2017-07-06T23:17:48.347+00:00","@version":1,"message":"Record _acme-challenge.some.example.com updated in ns-xxx.awsdns.com.","logger_name":"in.tazj.k8s.letsencrypt.util.DnsRecordObserver","thread_name":"Thread-5","level":"INFO","level_value":20000}
Exception in thread "Thread-5" java.lang.ClassCastException: in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$prepareDnsChallenge$cleanup$1 cannot be cast to java.lang.Runnable
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.prepareDnsChallenge(CertificateRequestHandler.kt:180)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.authorizeDomain(CertificateRequestHandler.kt:77)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.access$authorizeDomain(CertificateRequestHandler.kt:27)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:27)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
	at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:401)
	at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:734)
	at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:583)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.requestCertificate(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.handleCertificateRequest(ServiceManager.kt:64)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.access$handleCertificateRequest(ServiceManager.kt:20)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager$reconcileService$1.run(ServiceManager.kt:45)
	at java.lang.Thread.run(Thread.java:745)
{"@timestamp":"2017-07-06T23:17:48.357+00:00","@version":1,"message":"Record _acme-challenge.some.example.com updated in ns-xxx.awsdns.com.","logger_name":"in.tazj.k8s.letsencrypt.util.DnsRecordObserver","thread_name":"ForkJoinPool.commonPool-worker-0","level":"INFO","level_value":20000}

Watch certificate expiry and renew automatically

We know how long letsencrypt certificates are valid and it should be possible to store that information as an annotation on the secrets.

The controller should periodically check for certificates that are "close" (?) to expiry and renew them.

Re-evaluate naming of secrets

I'm not sure whether secrets should be named after the domain that they belong to or the service that requested them.

For example, a service "admin" exists in different environments (admin.test.foo.com and admin.prod.foo.com). The service admin-external requests the certificates.

This currently generates secrets named admin-test-foo-com-tls and admin-prod-foo-com-tls. This means that a webserver Deployment resource configuration is different between the environments.

Is that sensible? Is it possible to retrieve volume names from a ConfigMap? (i.e. having a ConfigMap per environment that maps these, therefore still having sensible secret names and having resource reusability).

Consider Kotlin rewrite

The current implementation makes heavy use of "spooky" features from Project Lombok (such as val).

It may make sense to just rewrite the project in Kotlin because that's more aligned with how I think anyways.

Support base domain configuration

This is specific to my use-case at the moment.

We have different clusters that are named "${environment}.${ourdomain}" and we want to provision records in those domains, however this makes it necessary to template Service objects with environment specific configuration.

It could be possible to add a toggle of sorts to set a base domain that other things are prepended to, unless an FQDN is specified in acme/certificate.

This could be either:

  1. Controller-level configuration (i.e. applies to all certificates)
  2. An annotation referencing a ConfigMap that is expected to contain some sort of acme-basedomain key. In that case only this ConfigMap needs to exist per environment.

Not yet sure which one I prefer.

Deprecate single-domain annotation syntax

Currently the type of the annotation field is technically something like Either<String, List<String>> with this silly line:

if (requestAnnotation.startsWith("[")) {

I don't remember why I added support for that but it should be removed.

As this is a breaking change I'm tagging it as 2.0.

Label resources appropriately

Created Secret resources should be labeled with something that identifies them as having been created by this controller.

Check that existing secret matches domains

Related to #21.

A user may expect that changing the list of requested domains updates the certificate. An annotation should be added to new certificates to provide this functionality.

Transient error: "Must agree to subscriber agreement"

This occurs sometimes when multiple certificates are requested at once. It is a minor issue as it retries afterwards.

In the log below, note the Using existing ACME user lines before the Created new ACME user. This appears to (sometimes) lead to errors because the agreement is not yet done.

Service s1 requesting certificates: [s1.domain.com]
Service s3 requesting certificates: [s3.domain.com]
Service s4 requesting certificates: [s4.domain.com]
Service s2 requesting certificates: [s2.domain.com]
Agreeing to Let's Encrypt subscriber agreement. Terms are available at https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
Using existing ACME user: https://acme-v01.api.letsencrypt.org/acme/reg/20226256
Using existing ACME user: https://acme-v01.api.letsencrypt.org/acme/reg/20226256
Using existing ACME user: https://acme-v01.api.letsencrypt.org/acme/reg/20226256
Exception in thread "Thread-3" in.tazj.k8s.letsencrypt.util.LetsencryptException: Must agree to subscriber agreement before any further actions
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.authorizeDomain(CertificateRequestHandler.kt:82)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.access$authorizeDomain(CertificateRequestHandler.kt:27)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:27)
	[SNIP: java.util.*]
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.requestCertificate(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.handleCertificateRequest(ServiceManager.kt:64)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.access$handleCertificateRequest(ServiceManager.kt:20)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager$reconcileService$1.run(ServiceManager.kt:45)
	at java.lang.Thread.run(Thread.java:745)

 Created new ACME user, URI: https://acme-v01.api.letsencrypt.org/acme/reg/20226256
 org.shredzone.acme4j.exception.AcmeUnauthorizedException: Must agree to subscriber agreement before any further actions
	at org.shredzone.acme4j.connector.DefaultConnection.createAcmeException(DefaultConnection.java:382)
	at org.shredzone.acme4j.connector.DefaultConnection.accept(DefaultConnection.java:199)
	at org.shredzone.acme4j.Registration.authorizeDomain(Registration.java:189)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.getAuthorization(CertificateRequestHandler.kt:90)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.authorizeDomain(CertificateRequestHandler.kt:68)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.access$authorizeDomain(CertificateRequestHandler.kt:27)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler$requestCertificate$1.accept(CertificateRequestHandler.kt:27)
	[SNIP: java.util.*]
	at in.tazj.k8s.letsencrypt.acme.CertificateRequestHandler.requestCertificate(CertificateRequestHandler.kt:41)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.handleCertificateRequest(ServiceManager.kt:64)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager.access$handleCertificateRequest(ServiceManager.kt:20)
	at in.tazj.k8s.letsencrypt.kubernetes.ServiceManager$reconcileService$1.run(ServiceManager.kt:45)
	at java.lang.Thread.run(Thread.java:745)

 Issuing new challenge for s4.domain.com
 Issuing new challenge for s1.domain.com
 Waiting for change in zone external-dns to finish. This may take some time.
 Issuing new challenge for s3.domain.com
 Waiting for change in zone external-dns to finish. This may take some time.
 Waiting for change in zone external-dns to finish. This may take some time.

AWS SDK prints unstructured log message on GCP

When probing for which platform we are running on, the AWS SDK prints an unstructured log message if it fails:

vincent@urdhva ~ % kubectl -n kube-system logs letsencrypt-controller-1039406318-q30r0 -f
{"@timestamp":"2017-05-04T20:48:34.955+00:00","@version":1,"message":"Detecting current cloud platform ...","logger_name":"in.tazj.k8s.letsencrypt.util.DetectCloudPlatform","thread_name":"main","level":"INFO","level_value":20000}
May 04, 2017 8:48:40 PM com.amazonaws.util.EC2MetadataUtils getItems
WARNING: Unable to retrieve the requested metadata.
{"@timestamp":"2017-05-04T20:48:51.742+00:00","@version":1,"message":"Cloud platform is Google Cloud Platform","logger_name":"in.tazj.k8s.letsencrypt.util.DetectCloudPlatform","thread_name":"main","level":"INFO","level_value":20000}

This can be fixed by excluding the AWS SDK logging facilities and putting in a bridge from whatever they use to slf4j.

Influence the cert filenames

I am trying to use this with https://github.com/kubernetes/ingress/blob/master/controllers/gce/README.md#frontend-https

In https://github.com/tazjin/kubernetes-letsencrypt#usage you write:

The certificate secret will contain four files named certificate.pem, chain.pem, key.pem and fullchain.pem. You can mount these into whatever application you use to terminate TLS.

Unfortunately GCE expects the files to be called tls.crt and tls.key. This is not a bug on kubernetes-letsencrypt as such, just mention it here to hear your thoughts.

Always determine authoritative NS from root

When validating updated DNS records the controller currently determines the authoritative nameservers for the zone via the DNS servers configured in the OS.

In case of something like a split-brain DNS setup with a public & private zone in Route53, the user could end up in a situation where the host running the controller is configured to resolve records from the private zone. In this case updates in the public zone will never become visible to the controller and the validation will fail.

Let's Encrypt always validates challenges starting from the root nameservers. To ensure that we actually go through the same path the controller should do the same thing.


See the discussion at the end of #61 for more information.

Create concatenated chain in secret

Some web servers like nginx want concatenated certificate chains. Right now the Letsencrypt certificate chain is in a separate file, there should be a concatenated version right away.

Travis builds twice?

I used the recommended Travis configuration snippet for differentiating between pull-requests and "normal" builds (because normal builds deploy to Docker Hub).

It looks like the conditional building on pull-requests works fine, but for normal builds everything is executed twice. That doesn't seem right!

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.