From bc3920b8b5e0f5d29f28b4c2f94b50b4d2c66a0a Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Sat, 20 Jun 2020 22:00:32 -0400 Subject: [PATCH 1/6] Cleanup docs acme-cli is no longer a thing so point to micronaut-starter and Micronaut Launch instead. --- README.md | 4 +- examples/hello-world-acme/README.md | 3 +- src/main/docs/guide/cli.adoc | 2 +- src/main/docs/guide/cli/installation.adoc | 1 - .../docs/guide/cli/installation/download.adoc | 10 --- .../guide/cli/installation/fromsource.adoc | 22 ------ .../guide/cli/installation/notwindows.adoc | 25 ------- .../docs/guide/cli/installation/windows.adoc | 12 --- src/main/docs/guide/cli/usage.adoc | 74 ++++++++++++++----- src/main/docs/guide/toc.yml | 10 --- 10 files changed, 60 insertions(+), 103 deletions(-) delete mode 100644 src/main/docs/guide/cli/installation.adoc delete mode 100644 src/main/docs/guide/cli/installation/download.adoc delete mode 100644 src/main/docs/guide/cli/installation/fromsource.adoc delete mode 100644 src/main/docs/guide/cli/installation/notwindows.adoc delete mode 100644 src/main/docs/guide/cli/installation/windows.adoc diff --git a/README.md b/README.md index ff9d9be4..af4ae4bc 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ the front runner for integration with Acme and is completely free. See the [stable](https://micronaut-projects.github.io/micronaut-acme/latest/guide) or [snapshot](https://micronaut-projects.github.io/micronaut-acme/snapshot/guide) documentation for more information. -## Acme Cli ## -Since ACME servers do require some pre setup there is a acme-cli subproject that can be found [here](https://github.com/micronaut-projects/micronaut-acme/tree/master/acme-cli). Which can help you create keys, create/deactivate accounts, etc. +## ACME Tooling ## +Since ACME servers do require some pre setup support has been baked into the micronaut-cli found [here](https://github.com/micronaut-projects/micronaut-starter). Which can help you create keys, create/deactivate accounts, etc. ## Example Application ## diff --git a/examples/hello-world-acme/README.md b/examples/hello-world-acme/README.md index 02d56715..ff77f26c 100644 --- a/examples/hello-world-acme/README.md +++ b/examples/hello-world-acme/README.md @@ -7,8 +7,7 @@ It contains a single endpoint found at `/helloWorld` but the important bits can ### Pre-reqs 1. You have created an account with Let's Encrypt 1. You have generated a domain key - 1. acme-cli project can help with steps 1 and 2 - 1. See [here](../acme-cli/README.md) + 1. microanut-cli (aka micronaut-starter) can help with steps 1 and 2 1. You have purchased a domain name and have a way to configure DNS. In the AWS example below we will use Route53. ### Build and Deploy : diff --git a/src/main/docs/guide/cli.adoc b/src/main/docs/guide/cli.adoc index 03f08e1c..72ecd664 100644 --- a/src/main/docs/guide/cli.adoc +++ b/src/main/docs/guide/cli.adoc @@ -1,2 +1,2 @@ To be able to secure your application using ACME there will be a few setup steps necessary before you can start using -the new certificates. A cli application can be found to assist in those steps, such as creating key pairs and creating accounts. +the new certificates. Support for ACME and this setup has been baked into micronaut-starter (https://github.com/micronaut-projects/micronaut-starter). diff --git a/src/main/docs/guide/cli/installation.adoc b/src/main/docs/guide/cli/installation.adoc deleted file mode 100644 index dd2795dc..00000000 --- a/src/main/docs/guide/cli/installation.adoc +++ /dev/null @@ -1 +0,0 @@ -Installation of the cli \ No newline at end of file diff --git a/src/main/docs/guide/cli/installation/download.adoc b/src/main/docs/guide/cli/installation/download.adoc deleted file mode 100644 index f0d2594e..00000000 --- a/src/main/docs/guide/cli/installation/download.adoc +++ /dev/null @@ -1,10 +0,0 @@ -A zip containing the cli is uploaded to maven central for release artifacts or jfrog oss repository for snapshot artifacts. - -Snapshots: -https://oss.jfrog.org/artifactory/oss-snapshot-local/io/micronaut/micronaut-acme-cli/ - -Releases: -https://repo1.maven.org/maven2/io/micronaut/micronaut-acme-cli/ - -Select the version you want to install and download the artifact with the name. -`micronaut-acme-cli-.zip` diff --git a/src/main/docs/guide/cli/installation/fromsource.adoc b/src/main/docs/guide/cli/installation/fromsource.adoc deleted file mode 100644 index d0cec38b..00000000 --- a/src/main/docs/guide/cli/installation/fromsource.adoc +++ /dev/null @@ -1,22 +0,0 @@ -Clone the repository found below -[source,bash] ----- -$ git clone git@github.com:micronaut-projects/micronaut-acme.git ----- - -`cd` into the `micronaut-acme` directory and run the following command: - -In a terminal, execute the following task -Unix -[source,bash] ----- -$ ./gradlew acme-cli:shadowDistZip ----- - -Windows -[source,bash] ----- -$ ./gradlew.bat acme-cli:shadowDistZip ----- - -The built cli application zip can be found under `micronaut-acme/acme-cli/build/distributions`. \ No newline at end of file diff --git a/src/main/docs/guide/cli/installation/notwindows.adoc b/src/main/docs/guide/cli/installation/notwindows.adoc deleted file mode 100644 index 1e97f3f2..00000000 --- a/src/main/docs/guide/cli/installation/notwindows.adoc +++ /dev/null @@ -1,25 +0,0 @@ -Once you have downloaded or built from source you now need to setup your PATH - -In your shell profile (~/.bash_profile if you are using the Bash shell), export the MICRONAUT_ACME_HOME directory and add the CLI path to your PATH: - -.bash_profile/.bashrc -[source,bash] ----- -export MICRONAUT_ACME_HOME=~/path/to/micronaut-acme-cli -export PATH="$PATH:$MICRONAUT_ACME_HOME/bin" ----- - -Reload your terminal or `source` your shell profile with `source`: - -[source,bash] ----- -> source ~/.bash_profile ----- - - -You should now be able to run the Micronaut CLI from the command prompt as follows: - -[source,bash] ----- -$ acme-cli --help ----- \ No newline at end of file diff --git a/src/main/docs/guide/cli/installation/windows.adoc b/src/main/docs/guide/cli/installation/windows.adoc deleted file mode 100644 index 0af02d05..00000000 --- a/src/main/docs/guide/cli/installation/windows.adoc +++ /dev/null @@ -1,12 +0,0 @@ -Once you have downloaded or built from source you now need to setup your PATH - -* Extract the binary to appropriate location (For example: `C:\micronaut-acme-cli`) -* Create an environment variable `MICRONAUT_ACME_HOME` which points to the installation directory i.e. `C:\micronaut-acme-cli` -* Update the `PATH` environment variable, append `%MICRONAUT_ACME_HOME%\bin`. - -You should now be able to run the Micronaut CLI from the command prompt as follows: - -[source,bash] ----- -$ acme-cli --help ----- \ No newline at end of file diff --git a/src/main/docs/guide/cli/usage.adoc b/src/main/docs/guide/cli/usage.adoc index 89f48c58..924f2399 100644 --- a/src/main/docs/guide/cli/usage.adoc +++ b/src/main/docs/guide/cli/usage.adoc @@ -1,3 +1,28 @@ +To use these functions you must first enable the `acme` feature in your app. + +== For a new app +Either at creation time you will need to select the `acme` feature + +Using the Micronaut CLI select the `acem` feature on creation. + +[source,bash] +---- +mn create-app --features acme hello-world +---- + +Or using Micronaut Launch https://micronaut.io/launch/ simply select `acme` feature before downloading your pre-built app. + +== For an existing app +Use the micronaut cli to do a `feature-diff` on an exiting app to show the changes needed +to enable the feature. + +ex. CLI Feature Diff +[source,bash] +---- +cd +mn feature-diff --features acme +---- + == Creating keypairs A utility to help with creating keypairs. This is akin to doing something like so with openssl @@ -13,22 +38,25 @@ Usage: [source,bash] ---- -Usage: acme-cli create-key [-h] [-k=] [-n=] [-s=] -Creates an keypair for use with account creation - -h, --help Show usage of this command +Usage: mn create-key [-fhvVx] [-k=] -n= [-s=] +Creates an keypair for use with ACME integration + -f, --force Whether to overwrite existing files + -h, --help Show this help message and exit. -k, --key-dir= Custom location on disk to put the key to be used with this account. - Default: /tmp + Default: src/main/resources -n, --key-name= Name of the key to be created - Default: acme.pem -s, --key-size= Size of the key to be generated Default: 4096 + -v, --verbose Create verbose output. + -V, --version Print version information and exit. + -x, --stacktrace Show full stack trace when exceptions occur. ---- == Creating an Account Creates a new account for a given ACME provider. This command will either create a new account keypair for you or you can pass -the account keypair that you have generated using the `acme-cli create-key` or via `openssl` or other means in as a parameter. +the account keypair that you have generated using the `mn create-key` or via `openssl` or other means in as a parameter. https://certbot.eff.org/[Certbot] or many of the other tools out there can also accomplish this step if you dont want to use this tool. @@ -36,15 +64,21 @@ Usage: [source,bash] ---- -Usage: create-account (-u= | --lets-encrypt-prod | --lets-encrypt-staging) [-h] - -e= [-k=] [-n=] +Usage: mn create-acme-account (-u= | --lets-encrypt-prod | --lets-encrypt-staging) + [-fhvVx] -e= [-k=] -n= [-s=] Creates a new account on the given ACME server -e, --email= Email address to create account with. - -h, --help Show usage of this command - -k, --key-dir= Directory to create/find the key to be used for this account. - Default: /tmp - -n, --key-name= Name of the key to be created/used - Default: acme.pem + -f, --force Whether to overwrite existing files + -h, --help Show this help message and exit. + -k, --key-dir= Custom location on disk to put the key to be used with this + account. + Default: src/main/resources + -n, --key-name= Name of the key to be created + -s, --key-size= Size of the key to be generated + Default: 4096 + -v, --verbose Create verbose output. + -V, --version Print version information and exit. + -x, --stacktrace Show full stack trace when exceptions occur. ACME server URL --lets-encrypt-prod Use the Let's Encrypt prod URL. --lets-encrypt-staging Use the Let's Encrypt staging URL @@ -59,14 +93,18 @@ Usage: [source,bash] ---- -Usage: deactivate-account (-u= | --lets-encrypt-prod | --lets-encrypt-staging) [-h] - [-k=] [-n=] +Usage: mn deactivate-acme-account (-u= | --lets-encrypt-prod | + --lets-encrypt-staging) [-fhvVx] [-k=] [-n=] Deactivates an existing ACME account - -h, --help Show usage of this command + -f, --force Whether to overwrite existing files + -h, --help Show this help message and exit. -k, --key-dir= Directory to find the key to be used for this account. - Default: /tmp + Default: src/main/resources -n, --key-name= Name of the key to be used - Default: acme.pem + Default: null + -v, --verbose Create verbose output. + -V, --version Print version information and exit. + -x, --stacktrace Show full stack trace when exceptions occur. ACME server URL --lets-encrypt-prod Use the Let's Encrypt prod URL. --lets-encrypt-staging Use the Let's Encrypt staging URL diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 0835130c..1b75a54e 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -11,16 +11,6 @@ challenges: title: DNS-01 cli: title: CLI - installation: - title: Installation - download: - title: Download - fromsource: - title: Build from source - windows: - title: Install through Binary on Windows - notwindows: - title: Install through Binary on Linux/MacOSx usage: title: Usage From b9d6f74e9c6b9ea9e49410dd6b2c592565f24b17 Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Mon, 22 Jun 2020 20:37:18 -0400 Subject: [PATCH 2/6] Project version 1.1.0.BUILD.SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 292c0285..3ad94afc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=1.0.1.BUILD-SNAPSHOT +projectVersion=1.1.0.BUILD-SNAPSHOT cglibVersion=2.2.2 developers=Nathan Zender githubBranch=master From 29c5488cf9549d2ef50bc71528a6cbe4d11a4e7f Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Tue, 23 Jun 2020 04:08:46 +0000 Subject: [PATCH 3/6] Update dependencies --- acme/build.gradle | 4 ++-- build.gradle | 2 +- gradle.properties | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/build.gradle b/acme/build.gradle index b66fbe79..221197d6 100644 --- a/acme/build.gradle +++ b/acme/build.gradle @@ -16,9 +16,9 @@ dependencies { documentation "org.codehaus.groovy:groovy-templates:${groovyVersion}" documentation "org.codehaus.groovy:groovy-dateutil:${groovyVersion}" - implementation group: 'io.netty', name: 'netty-tcnative-boringssl-static', version: '2.0.25.Final' + implementation group: 'io.netty', name: 'netty-tcnative-boringssl-static', version: '2.0.31.Final' - testImplementation "org.testcontainers:spock:1.13.0" + testImplementation "org.testcontainers:spock:1.14.3" testImplementation("io.micronaut:micronaut-http-client") testImplementation("org.codehaus.groovy:groovy-dateutil:$groovyVersion") testImplementation("org.spockframework:spock-core:${spockVersion}") { diff --git a/build.gradle b/build.gradle index 6152c6c6..433cb422 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath "io.micronaut.build:micronaut-gradle-plugins:2.0.0.RC10" - classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0' + classpath 'com.github.jengelman.gradle.plugins:shadow:6.0.0' } } diff --git a/gradle.properties b/gradle.properties index 292c0285..22cba4c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ projectVersion=1.0.1.BUILD-SNAPSHOT -cglibVersion=2.2.2 +cglibVersion=3.3.0 developers=Nathan Zender githubBranch=master githubCoreBranch=master @@ -8,10 +8,10 @@ githubSlug=micronaut-projects/micronaut-acme groovyVersion=2.5.11 logbackClassicVersion=1.2.3 micronautVersion=1.3.5 -micronautTestVersion=1.1.5 +micronautTestVersion=1.2.0 micronautDocsVersion=1.0.23 micronautBuildVersion=1.1.5 -objenesisVersion=1.4 +objenesisVersion=3.1 htmlSanityCheckVersion=0.9.7 acmeVersion=2.9 spockVersion=1.3-groovy-2.5 From df56ab07a8c544521bfded694802f6b72c48aec3 Mon Sep 17 00:00:00 2001 From: Nathan Zender Date: Mon, 22 Jun 2020 22:47:55 -0400 Subject: [PATCH 4/6] Moving and shakin Instead of doing everything post app startup we can move some things into pre-app startup. Things moved: 1. Login and ordering a certificate if needed 2. If cert already downloaded and ready set it up Things that cannot be moved: 1. Authorizations, reason this cannot be moved is because it needs to be able to respond from requests from the ACME server. --- .../background/AcmeCertRefresherTask.java | 83 ++++++++++++------- .../micronaut/acme/services/AcmeService.java | 28 +++++-- ...cmeCertRefresherTaskSetsTimeoutSpec.groovy | 15 ++-- .../acme/AcmeCertRefresherTaskUnitSpec.groovy | 26 ++++-- 4 files changed, 105 insertions(+), 47 deletions(-) diff --git a/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java b/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java index 60fcfcc8..d75f225a 100644 --- a/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java +++ b/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java @@ -17,10 +17,13 @@ import io.micronaut.acme.AcmeConfiguration; import io.micronaut.acme.services.AcmeService; +import io.micronaut.context.event.StartupEvent; +import io.micronaut.http.server.exceptions.ServerStartupException; import io.micronaut.runtime.event.ApplicationStartupEvent; import io.micronaut.runtime.event.annotation.EventListener; import io.micronaut.runtime.exceptions.ApplicationStartupException; import io.micronaut.scheduling.annotation.Scheduled; +import org.shredzone.acme4j.Order; import org.shredzone.acme4j.exception.AcmeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +45,7 @@ public final class AcmeCertRefresherTask { private AcmeService acmeService; private final AcmeConfiguration acmeConfiguration; + private Order order; /** * Constructs a new Acme cert refresher background task. @@ -66,36 +70,71 @@ void backgroundRenewal() throws AcmeException { if (LOG.isDebugEnabled()) { LOG.debug("Running background/scheduled renewal process"); } - renewCertIfNeeded(); + if (!acmeConfiguration.isTosAgree()) { + throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); + } + List domains = getDomains(); + if (needsToOrderNewCertificate()) { + Order order = acmeService.orderCertificate(domains); + acmeService.authorizeCertificate(domains, order); + } } - /** - * Checks to see if certificate needs renewed on app startup. - * - * @param startupEvent Startup event - */ @EventListener - void onStartup(ApplicationStartupEvent startupEvent) { + void onServerStartup(StartupEvent startupEvent) { try { if (LOG.isDebugEnabled()) { - LOG.debug("Running startup renewal process"); + LOG.debug("Running server startup setup process"); + } + if (!acmeConfiguration.isTosAgree()) { + throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); + } + if (needsToOrderNewCertificate()) { + order = acmeService.orderCertificate(getDomains()); + } else { + acmeService.setupCurrentCertificate(); } - renewCertIfNeeded(); } catch (Exception e) { LOG.error("Failed to initialize certificate for SSL no requests would be secure. Stopping application", e); - throw new ApplicationStartupException("Failed to start due to SSL configuration issue.", e); + throw new ServerStartupException("Failed to start due to SSL configuration issue.", e); } } /** - * Does the work to actually renew the certificate if it needs to be done. - * @throws AcmeException if any issues occur during certificate renewal + * Checks to see if certificate needs renewed on app startup. + * + * @param startupEvent Startup event */ - protected void renewCertIfNeeded() throws AcmeException { - if (!acmeConfiguration.isTosAgree()) { - throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); + @EventListener + void onStartup(ApplicationStartupEvent startupEvent) { + if (needsToOrderNewCertificate()) { + try { + if (LOG.isDebugEnabled()) { + LOG.debug("Running application startup order/authorization process"); + } + acmeService.authorizeCertificate(getDomains(), order); + } catch (Exception e) { + LOG.error("Failed to initialize certificate for SSL no requests would be secure. Stopping application", e); + throw new ApplicationStartupException("Failed to start due to SSL configuration issue.", e); + } + } + } + + private boolean needsToOrderNewCertificate() { + boolean orderCertificate = false; + X509Certificate currentCertificate = acmeService.getCurrentCertificate(); + if (currentCertificate != null) { + long daysTillExpiration = ChronoUnit.SECONDS.between(Instant.now(), currentCertificate.getNotAfter().toInstant()); + if (daysTillExpiration <= acmeConfiguration.getRenewWitin().getSeconds()) { + orderCertificate = true; + } + } else { + orderCertificate = true; } + return orderCertificate; + } + private List getDomains() { List domains = new ArrayList<>(); for (String domain : acmeConfiguration.getDomains()) { domains.add(domain); @@ -107,18 +146,6 @@ protected void renewCertIfNeeded() throws AcmeException { domains.add(baseDomain); } } - - X509Certificate currentCertificate = acmeService.getCurrentCertificate(); - if (currentCertificate != null) { - long daysTillExpiration = ChronoUnit.SECONDS.between(Instant.now(), currentCertificate.getNotAfter().toInstant()); - - if (daysTillExpiration <= acmeConfiguration.getRenewWitin().getSeconds()) { - acmeService.orderCertificate(domains); - } else { - acmeService.setupCurrentCertificate(); - } - } else { - acmeService.orderCertificate(domains); - } + return domains; } } diff --git a/acme/src/main/java/io/micronaut/acme/services/AcmeService.java b/acme/src/main/java/io/micronaut/acme/services/AcmeService.java index dcdf2bd2..4018e4e2 100644 --- a/acme/src/main/java/io/micronaut/acme/services/AcmeService.java +++ b/acme/src/main/java/io/micronaut/acme/services/AcmeService.java @@ -143,10 +143,10 @@ public X509Certificate getCurrentCertificate() { * * @param domains List of domains to order a certificate for * @throws AcmeException if any issues occur during ordering of certificate + * + * @return order for the given list of domains */ - public void orderCertificate(List domains) throws AcmeException { - AtomicInteger orderRetryAttempts = new AtomicInteger(acmeConfiguration.getOrder().getRefreshAttempts()); - + public Order orderCertificate(List domains) throws AcmeException { Session session = new Session(acmeServerUrl); if (timeout != null) { session.networkSettings().setTimeout(timeout); @@ -156,14 +156,26 @@ public void orderCertificate(List domains) throws AcmeException { try { accountKeyPair = getKeyPairFromConfigValue(this.accountKeyString); } catch (IOException e) { - if (LOG.isErrorEnabled()) { - LOG.error("ACME certificate order failed. Failed to read the account keys", e); - } - return; + throw new AcmeException("ACME certificate order failed. Failed to read the account keys", e); } Login login = doLogin(session, accountKeyPair); - Order order = createOrder(domains, login); + return createOrder(domains, login); + } + + /** + * Authorizes an order and if successful emits a certificate via an event. + * + * @param domains List of domains to order a certificate for + * @param order acme order for the given set of domains + * @throws AcmeException if any issues occur during authorization of a certificate order + */ + public void authorizeCertificate(List domains, Order order) throws AcmeException { + if (order == null) { + throw new AcmeException("Order is required before you can authorize it."); + } + + AtomicInteger orderRetryAttempts = new AtomicInteger(acmeConfiguration.getOrder().getRefreshAttempts()); for (Authorization auth : order.getAuthorizations()) { try { authorize(auth); diff --git a/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskSetsTimeoutSpec.groovy b/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskSetsTimeoutSpec.groovy index 1b76813c..f59c79ef 100644 --- a/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskSetsTimeoutSpec.groovy +++ b/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskSetsTimeoutSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.acme import io.micronaut.context.ApplicationContext import io.micronaut.core.io.socket.SocketUtils +import io.micronaut.http.server.exceptions.ServerStartupException import io.micronaut.mock.slow.SlowAcmeServer import io.micronaut.mock.slow.SlowServerConfig import io.micronaut.runtime.exceptions.ApplicationStartupException @@ -98,7 +99,7 @@ class AcmeCertRefresherTaskSetsTimeoutSpec extends Specification { } @Unroll - def "validate timeout applied if signup is slow"(SlowServerConfig config) { + def "validate timeout applied if signup is slow"(SlowServerConfig config, Class exType) { given: "we have all the ports we could ever need" expectedHttpPort = SocketUtils.findAvailableTcpPort() expectedSecurePort = SocketUtils.findAvailableTcpPort() @@ -121,7 +122,9 @@ class AcmeCertRefresherTaskSetsTimeoutSpec extends Specification { "test") then: "we get network errors b/c of the timeout" - ApplicationStartupException ex = thrown() + def ex = thrown(Throwable) + + ex.class == exType def ane = ExceptionUtils.getThrowables(ex).find { it instanceof AcmeNetworkException } ane?.message == "Network error" @@ -135,10 +138,10 @@ class AcmeCertRefresherTaskSetsTimeoutSpec extends Specification { mockAcmeServer?.stop() where: - config | _ - new ActualSlowServerConfig(slowSignup: true) | _ - new ActualSlowServerConfig(slowOrdering: true) | _ - new ActualSlowServerConfig(slowAuthorization: true) | _ + config | exType + new ActualSlowServerConfig(slowSignup: true) | ServerStartupException + new ActualSlowServerConfig(slowOrdering: true) | ServerStartupException + new ActualSlowServerConfig(slowAuthorization: true) | ApplicationStartupException } class ActualSlowServerConfig implements SlowServerConfig { diff --git a/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy b/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy index d9d1c827..47df6047 100644 --- a/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy +++ b/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.acme import io.micronaut.acme.background.AcmeCertRefresherTask import io.micronaut.acme.services.AcmeService +import io.micronaut.http.server.exceptions.ServerStartupException import io.micronaut.runtime.EmbeddedApplication import io.micronaut.runtime.event.ApplicationStartupEvent import io.micronaut.runtime.exceptions.ApplicationStartupException @@ -23,7 +24,22 @@ class AcmeCertRefresherTaskUnitSpec extends Specification { def task = new AcmeCertRefresherTask(Mock(AcmeService), Mock(AcmeConfiguration)) when: - task.renewCertIfNeeded() + task.onServerStartup(null) + + then: + def ex = thrown(ServerStartupException.class) + + Throwable rootEx = ExceptionUtils.getRootCause(ex) + rootEx instanceof IllegalStateException + rootEx.message == "Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"acme.tos-agree\" to \"true\" in configuration once complete" + } + + def "throw exception if TOS has not been accepted when doing background reneal"() { + given: + def task = new AcmeCertRefresherTask(Mock(AcmeService), Mock(AcmeConfiguration)) + + when: + task.backgroundRenewal() then: def ex = thrown(IllegalStateException.class) @@ -39,7 +55,7 @@ class AcmeCertRefresherTaskUnitSpec extends Specification { def task = new AcmeCertRefresherTask(mockAcmeSerivce, config) when: - task.renewCertIfNeeded() + task.onServerStartup(null) then: 1 * mockAcmeSerivce.getCurrentCertificate() >> new SelfSignedCertificate(expectedDomain, new Date(), new Date() + 31).cert() @@ -56,7 +72,7 @@ class AcmeCertRefresherTaskUnitSpec extends Specification { def task = new AcmeCertRefresherTask(mockAcmeSerivce, config) when: - task.renewCertIfNeeded() + task.onServerStartup(null) then: 1 * mockAcmeSerivce.getCurrentCertificate() >> new SelfSignedCertificate(expectedDomain, new Date(), new Date() + 31).cert() @@ -77,10 +93,10 @@ class AcmeCertRefresherTaskUnitSpec extends Specification { def task = new AcmeCertRefresherTask(mockAcmeSerivce, config) when: - task.onStartup(new ApplicationStartupEvent(Mock(EmbeddedApplication))) + task.onServerStartup(null) then: - def ex = thrown(ApplicationStartupException) + def ex = thrown(ServerStartupException) ex.message == "Failed to start due to SSL configuration issue." and: From 4a08b546e15475122580f2f60eebf8a1e8f40ba5 Mon Sep 17 00:00:00 2001 From: Guillermo Calvo Date: Tue, 8 Aug 2023 15:20:26 +0200 Subject: [PATCH 5/6] Keep backward compatibility --- .../background/AcmeCertRefresherTask.java | 25 ++++++++++++------- .../micronaut/acme/services/AcmeService.java | 15 ++++++++++- .../acme/AcmeCertRefresherTaskUnitSpec.groovy | 7 ++---- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java b/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java index 7d36efef..a5200b49 100644 --- a/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java +++ b/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java @@ -70,14 +70,7 @@ void backgroundRenewal() throws AcmeException { if (LOG.isDebugEnabled()) { LOG.debug("Running background/scheduled renewal process"); } - if (!acmeConfiguration.isTosAgree()) { - throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); - } - List domains = getDomains(); - if (needsToOrderNewCertificate()) { - Order order = acmeService.orderCertificate(domains); - acmeService.authorizeCertificate(domains, order); - } + renewCertIfNeeded(); } @EventListener @@ -90,7 +83,7 @@ void onServerStartup(StartupEvent startupEvent) { throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); } if (needsToOrderNewCertificate()) { - order = acmeService.orderCertificate(getDomains()); + order = acmeService.createOrder(getDomains()); } else { acmeService.setupCurrentCertificate(); } @@ -148,4 +141,18 @@ private List getDomains() { } return domains; } + + /** + * Does the work to actually renew the certificate if it needs to be done. + * @throws AcmeException if any issues occur during certificate renewal + */ + protected void renewCertIfNeeded() throws AcmeException { + if (!acmeConfiguration.isTosAgree()) { + throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); + } + List domains = getDomains(); + if (needsToOrderNewCertificate()) { + acmeService.orderCertificate(domains); + } + } } diff --git a/acme/src/main/java/io/micronaut/acme/services/AcmeService.java b/acme/src/main/java/io/micronaut/acme/services/AcmeService.java index 382a2fdc..fba08699 100644 --- a/acme/src/main/java/io/micronaut/acme/services/AcmeService.java +++ b/acme/src/main/java/io/micronaut/acme/services/AcmeService.java @@ -167,6 +167,17 @@ protected Optional getFullCertificateChain() { return Optional.empty(); } + /** + * Orders a new certificate using ACME protocol. + * + * @param domains List of domains to order a certificate for + * @throws AcmeException if any issues occur during ordering of certificate + */ + public void orderCertificate(List domains) throws AcmeException { + final Order order = createOrder(domains); + authorizeCertificate(domains, order); + } + /** * Orders a new certificate using ACME protocol. * @@ -174,8 +185,9 @@ protected Optional getFullCertificateChain() { * @throws AcmeException if any issues occur during ordering of certificate * * @return order for the given list of domains + * @since 4.1 */ - public Order orderCertificate(List domains) throws AcmeException { + public Order createOrder(List domains) throws AcmeException { Session session = new Session(acmeServerUrl); if (timeout != null) { session.networkSettings().setTimeout(timeout); @@ -198,6 +210,7 @@ public Order orderCertificate(List domains) throws AcmeException { * @param domains List of domains to order a certificate for * @param order acme order for the given set of domains * @throws AcmeException if any issues occur during authorization of a certificate order + * @since 4.1 */ public void authorizeCertificate(List domains, Order order) throws AcmeException { if (order == null) { diff --git a/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy b/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy index 048e6f3b..0a28061f 100644 --- a/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy +++ b/acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy @@ -3,9 +3,6 @@ package io.micronaut.acme import io.micronaut.acme.background.AcmeCertRefresherTask import io.micronaut.acme.services.AcmeService import io.micronaut.http.server.exceptions.ServerStartupException -import io.micronaut.runtime.EmbeddedApplication -import io.micronaut.runtime.event.ApplicationStartupEvent -import io.micronaut.runtime.exceptions.ApplicationStartupException import io.netty.handler.ssl.util.SelfSignedCertificate import org.shredzone.acme4j.exception.AcmeException import org.testcontainers.shaded.org.apache.commons.lang3.exception.ExceptionUtils @@ -75,7 +72,7 @@ class AcmeCertRefresherTaskUnitSpec extends Specification { then: 1 * mockAcmeSerivce.getCurrentCertificate() >> new SelfSignedCertificate(expectedDomain, new Date(), new Date() + 31).cert() - 1 * mockAcmeSerivce.orderCertificate([expectedDomain]) + 1 * mockAcmeSerivce.createOrder([expectedDomain]) where: daysToRenew | description @@ -100,7 +97,7 @@ class AcmeCertRefresherTaskUnitSpec extends Specification { and: 1 * mockAcmeSerivce.getCurrentCertificate() >> null - 1 * mockAcmeSerivce.orderCertificate(expectedDomains) >> { List domains -> + 1 * mockAcmeSerivce.createOrder(expectedDomains) >> { List domains -> throw new AcmeException("Failed to do some ACME related task") } } From 68654685603a715b1a88291e01f38196a78cce25 Mon Sep 17 00:00:00 2001 From: Guillermo Calvo Date: Tue, 8 Aug 2023 15:39:42 +0200 Subject: [PATCH 6/6] Small refactor --- .../background/AcmeCertRefresherTask.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java b/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java index a5200b49..2abaf8bc 100644 --- a/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java +++ b/acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java @@ -29,11 +29,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + +import static java.time.Instant.now; +import static java.time.temporal.ChronoUnit.SECONDS; /** * Background task to automatically refresh the certificates from an ACME server on a configurable interval. @@ -114,17 +115,9 @@ void onStartup(ApplicationStartupEvent startupEvent) { } private boolean needsToOrderNewCertificate() { - boolean orderCertificate = false; - X509Certificate currentCertificate = acmeService.getCurrentCertificate(); - if (currentCertificate != null) { - long daysTillExpiration = ChronoUnit.SECONDS.between(Instant.now(), currentCertificate.getNotAfter().toInstant()); - if (daysTillExpiration <= acmeConfiguration.getRenewWitin().getSeconds()) { - orderCertificate = true; - } - } else { - orderCertificate = true; - } - return orderCertificate; + return Optional.ofNullable(acmeService.getCurrentCertificate()) + .map(c -> SECONDS.between(now(), c.getNotAfter().toInstant()) <= acmeConfiguration.getRenewWitin().getSeconds()) + .orElse(true); } private List getDomains() {