diff options
author | Simon Rettberg | 2024-10-08 16:25:56 +0200 |
---|---|---|
committer | Simon Rettberg | 2024-10-08 16:25:56 +0200 |
commit | 76dc961f86cc884b815aa28a436add1c128085fa (patch) | |
tree | d00b23243bd0a530d630144e468b014f39be51b7 | |
parent | Add comment (diff) | |
download | tmlite-bwlp-76dc961f86cc884b815aa28a436add1c128085fa.tar.gz tmlite-bwlp-76dc961f86cc884b815aa28a436add1c128085fa.tar.xz tmlite-bwlp-76dc961f86cc884b815aa28a436add1c128085fa.zip |
[LighttpdHttps] Add support for ACME
-rwxr-xr-x | scripts/install-https | 142 | ||||
-rwxr-xr-x | scripts/system-backup | 2 | ||||
-rw-r--r-- | src/main/java/org/openslx/taskmanager/tasks/LighttpdHttps.java | 245 |
3 files changed, 334 insertions, 55 deletions
diff --git a/scripts/install-https b/scripts/install-https index 0afeb58..e6daeea 100755 --- a/scripts/install-https +++ b/scripts/install-https @@ -4,6 +4,27 @@ declare -rg CERT_KEY_FILE="/etc/lighttpd/server.pem" declare -rg PUB_CERT_FILE="/etc/lighttpd/pub-cert.pem" declare -rg CHAIN_FILE="/etc/lighttpd/chain.pem" declare -rg REDIR_FLAG="/etc/lighttpd/redirect.flag" +declare -rg RELOAD_FLAG="/home/taskmanager/acme-reload" +declare -rg ACME_KEY="/home/taskmanager/certs/key.pem" +declare -rg ACME_FULLCHAIN="/home/taskmanager/certs/fullchain.pem" +declare -rg WWW="/srv/openslx/www" + +declare -rga acme_common=( + -w "$WWW" + --key-file "$ACME_KEY" + --fullchain-file "$ACME_FULLCHAIN" + --reloadcmd "touch '$RELOAD_FLAG'" +) + +declare -a tmpfiles= +trap 'rm -rf -- "${tmpfiles[@]}"' EXIT + +is_readable () +{ + [ -r "$1" ] && [ -f "$1" ] && return 0 + echo "Not a file, or not readable: $1" + return 1 +} op_disable () { @@ -17,19 +38,17 @@ op_test () [ $# -eq 2 ] || exit 1 local K=$1 local C=$2 - [ -r "$K" ] || exit 2 - [ -r "$C" ] || exit 3 + is_readable "$K" || exit 2 + is_readable "$C" || exit 3 # Encrypt something, then decrypt again and compare local TEST_IN=$(mktemp --tmpdir bwlp-XXXXXXXX) local TEST_OUT=$(mktemp --tmpdir bwlp-XXXXXXXX) - local TEST_DIFF=$(mktemp --tmpdir bwlp-XXXXXXXX) [ -z "$TEST_IN" ] && exit 4 [ -z "$TEST_OUT" ] && exit 5 - [ -z "$TEST_DIFF" ] && exit 6 + tmpfiles+=( "$TEST_IN" "$TEST_OUT" ) date > "$TEST_IN" - openssl smime -encrypt -binary -aes-256-cbc -in "$TEST_IN" -out "$TEST_OUT" -outform DER "$C" || exit 7 - openssl smime -decrypt -binary -in "$TEST_OUT" -inform DER -out "$TEST_DIFF" -inkey "$K" || exit 8 - diff -q "$TEST_IN" "$TEST_DIFF" || exit 9 + openssl smime -sign -in "$TEST_IN" -out "$TEST_OUT" -signer "$C" -inkey "$K" || exit 7 + openssl smime -verify -noverify -in "$TEST_OUT" -out /dev/null -signer "$C" || exit 8 exit 0 # No restart either way } @@ -39,8 +58,8 @@ op_import () local K=$1 local C=$2 local CHAIN=$3 - [ -r "$K" ] || exit 2 - [ -r "$C" ] || exit 3 + is_readable "$K" || exit 2 + is_readable "$C" || exit 3 rm -f -- "$CHAIN_FILE" # Create server.pem { @@ -55,11 +74,85 @@ op_import () cat "$K" } > "$CERT_KEY_FILE" chmod 0600 "$CERT_KEY_FILE" || exit 4 - rm -f -- "$C" "$K" post_setup_hook return 0 } +op_acme_issue () +{ + [ $# -lt 3 ] && exit 1 + local i + local provider="$1" + local mail="$2" + shift 2 + declare -a domains=() + set -x + for i; do + domains+=( -d "$i" ) + done + rm -f -- "$RELOAD_FLAG" + if [ -n "$ACME_WIPE" ]; then + rm -rf -- /home/taskmanager/.acme.sh/* + rm -rf -- /home/taskmanager/certs/* + touch /home/taskmanager/.acme.sh/account.conf + chown taskmanager:taskmanager /home/taskmanager/.acme.sh/account.conf + else + rm -rf -- /home/taskmanager/.acme.sh/???*.* + rm -rf -- /home/taskmanager/certs/* + fi + mkdir -p "$WWW/.well-known/acme-challenge" + chown taskmanager:taskmanager "$WWW/.well-known/acme-challenge" + chmod 0700 /home/taskmanager/certs + if [ -n "$ACME_KEY_ID" ] && [ -n "$ACME_HMAC_KEY" ]; then + sudo -u taskmanager -n ACCOUNT_EMAIL="$mail" \ + /opt/openslx/acme.sh --register-account --server "$provider" --eab-kid "$ACME_KEY_ID" --eab-hmac-key "$ACME_HMAC_KEY" + fi + tmpfiles+=( "$RELOAD_FLAG" ) + sudo -u taskmanager -n ACCOUNT_EMAIL="$mail" \ + /opt/openslx/acme.sh --server "$provider" --issue "${domains[@]}" "${acme_common[@]}" + i=$? + if [ -e "$RELOAD_FLAG" ]; then + # Renew must have happened + op_import "$ACME_KEY" "$ACME_FULLCHAIN" + return 0 + fi + # No reload in other case, exit + exit "$i" +} + +op_acme_renew () +{ + mkdir -p "$WWW/.well-known/acme-challenge" + chown taskmanager:taskmanager "$WWW/.well-known/acme-challenge" + rm -f -- "$RELOAD_FLAG" + tmpfiles+=( "$RELOAD_FLAG" ) + sudo -u taskmanager -n /opt/openslx/acme.sh --renew --domain "$1" "${acme_common[@]}" + i=$? + (( i == 2 )) && i=0 + if (( i != 0 )) || ! [ -e "$ACME_FULLCHAIN" ] || ! [ -e "$ACME_KEY" ] \ + || ! openssl x509 -checkend 600 -in "$ACME_FULLCHAIN"; then + sudo -u taskmanager -n /opt/openslx/acme.sh --renew --force --domain "$1" "${acme_common[@]}" + i=$? + fi + if [ -e "$RELOAD_FLAG" ]; then + # Renew must have happened + op_import "$ACME_KEY" "$ACME_FULLCHAIN" + return 0 + fi + # No reload in other case, exit + + exit "$i" +} + +op_acme_try_enable () +{ + [ -s "$ACME_KEY" ] || exit 1 + [ -s "$ACME_FULLCHAIN" ] || exit 2 + openssl x509 -checkend 600 -noout -in "$ACME_FULLCHAIN" || exit 3 + ( op_test "$ACME_KEY" "$ACME_FULLCHAIN" ) || exit 4 + op_import "$ACME_KEY" "$ACME_FULLCHAIN" +} + op_random () { [ -z "$1" ] && exit 1 @@ -85,6 +178,9 @@ setup_redirect () fi } +ACME_KEY_ID= +ACME_HMAC_KEY= +ACME_WIPE= RE_ONLY= REDIR= while true; do @@ -95,6 +191,17 @@ while true; do --redirect) REDIR=truh ;; + --acme-wipe) + ACME_WIPE=1 + ;; + --acme-key-id) + ACME_KEY_ID="$2" + shift + ;; + --acme-hmac-key) + ACME_HMAC_KEY="$2" + shift + ;; *) break ;; @@ -112,8 +219,14 @@ if [ -z "$RE_ONLY" ]; then case "$OP" in --random) op_random "$@" ;; --test) op_test "$@" ;; - --import) op_import "$@" ;; + --import) + tmpfiles+=( "$@" ) + op_import "$@" + ;; --disable) op_disable ;; + --acme-issue) op_acme_issue "$@" ;; + --acme-renew) op_acme_renew "$@" ;; + --acme-try-enable) op_acme_try_enable ;; *) echo "Invalid operation: $1" exit 1 @@ -122,8 +235,9 @@ if [ -z "$RE_ONLY" ]; then fi -sleep .5 -systemctl restart lighttpd +( + sleep .5 + systemctl restart lighttpd +) & exit 0 - diff --git a/scripts/system-backup b/scripts/system-backup index 52422bb..5cc7d7e 100755 --- a/scripts/system-backup +++ b/scripts/system-backup @@ -51,6 +51,8 @@ FILELIST=( "/etc/lighttpd/server.pem" "/etc/lighttpd/chain.pem" "/etc/lighttpd/pub-cert.pem" + "/home/taskmanager/.acme.sh" + "/home/taskmanager/certs" ) tar --ignore-failed-read -k -c -p -z -f "files.tgz" "${FILELIST[@]}" diff --git a/src/main/java/org/openslx/taskmanager/tasks/LighttpdHttps.java b/src/main/java/org/openslx/taskmanager/tasks/LighttpdHttps.java index 08fac2a..20ef463 100644 --- a/src/main/java/org/openslx/taskmanager/tasks/LighttpdHttps.java +++ b/src/main/java/org/openslx/taskmanager/tasks/LighttpdHttps.java @@ -3,9 +3,11 @@ package org.openslx.taskmanager.tasks; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.openslx.satserver.util.Exec; +import org.openslx.satserver.util.Exec.ExecCallback; import org.openslx.satserver.util.Util; import org.openslx.taskmanager.api.AbstractTask; @@ -19,6 +21,8 @@ public class LighttpdHttps extends AbstractTask { private Output status = new Output(); + + // --- User-supplied cert --- @Expose private String importcert = null; @@ -26,16 +30,43 @@ public class LighttpdHttps extends AbstractTask private String importkey = null; @Expose private String importchain = null; - + + // --- Let's encrypt or similar (ACME) --- + + @Expose + private String acmeMode = null; + @Expose + private String acmeMail = null; + @Expose + private String[] acmeDomains = null; + @Expose + private String acmeProvider = null; + @Expose + private boolean acmeWipeAll = false; + @Expose + private String acmeKeyId = null; + @Expose + private String acmeHmacKey = null; + + // ---- Self-signed ---- + + /** IP address to put in self-signed cert */ @Expose private String proxyip = null; - + + // ------ Force HTTPS? ------ + + /** Enable redirect from HTTP to HTTPS? */ @Expose private boolean redirect; + /** Only set the requested redirect mode, nothing else */ @Expose private boolean redirectOnly; + + // ------- - private List<String> baseCmd = Arrays.asList( new String[] { "sudo", "-n", "-u", "root", "/opt/taskmanager/scripts/install-https" } ); + private static List<String> BASE_CMD = Collections.unmodifiableList( Arrays.asList( new String[] { + "sudo", "-n", "-u", "root", "/opt/taskmanager/scripts/install-https" } ) ); @Override protected boolean initTask() @@ -51,14 +82,19 @@ public class LighttpdHttps extends AbstractTask return setRedirect(); if ( this.importcert != null && this.importkey != null && !this.importcert.isEmpty() && !this.importkey.isEmpty() ) return createFromInput(); + if ( this.acmeMode != null ) + return handleAcme(); if ( this.proxyip != null && !this.proxyip.isEmpty() ) return createRandom(); return disableHttps(); } + /** + * Create a new self-signed certificate and use that. + */ private boolean createRandom() { - List<String> cmd = new ArrayList<>( baseCmd ); + List<String> cmd = new ArrayList<>( BASE_CMD ); if ( this.redirect ) { cmd.add( "--redirect" ); } @@ -66,19 +102,21 @@ public class LighttpdHttps extends AbstractTask cmd.add( this.proxyip ); int ret = Exec.sync( 45, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { - status.error = "generator exited with code " + ret; + status.processStdOut( "generator exited with code " + ret ); return false; } return true; } + /** + * Create HTTPS config from cert/key/chain provided in task data. + */ private boolean createFromInput() { // Import supplied certificate and key. Test if they are valid first File tmpKey = null; File tmpCert = null; File tmpChain = null; - List<String> cmd; try { try { tmpCert = File.createTempFile( "bwlp-", ".pem" ); @@ -90,53 +128,163 @@ public class LighttpdHttps extends AbstractTask Util.writeStringToFile( tmpChain, this.importchain ); } } catch ( Exception e ) { - status.error = "Could not create temporary files: " + e.getMessage(); + status.processStdOut( "Could not create temporary files: " + e.getMessage() ); return false; } - int ret; - cmd = new ArrayList<>( baseCmd ); - cmd.add( "--test" ); - cmd.add( tmpKey.getAbsolutePath() ); - cmd.add( tmpCert.getAbsolutePath() ); - ret = Exec.sync( 45, cmd.toArray( new String[ cmd.size() ] ) ); - if ( ret != 0 ) { - status.error = "Given key and certificate do not match, or have invalid format (exit code: " + ret + ")"; - return false; + return createFromFiles( tmpKey, tmpCert, tmpChain ); + } finally { + if ( tmpKey != null ) { + tmpKey.delete(); } - cmd = new ArrayList<>( baseCmd ); - if ( this.redirect ) { - cmd.add( "--redirect" ); + if ( tmpCert != null ) { + tmpCert.delete(); } - cmd.add( "--import" ); - cmd.add( tmpKey.getAbsolutePath() ); - cmd.add( tmpCert.getAbsolutePath() ); if ( tmpChain != null ) { - cmd.add( tmpChain.getAbsolutePath() ); + tmpChain.delete(); } - ret = Exec.sync( 45, cmd.toArray( new String[ cmd.size() ] ) ); - if ( ret != 0 ) { - status.error = "import exited with code " + ret; - return false; - } - return true; - } finally { - if ( tmpKey != null ) - tmpKey.delete(); - if ( tmpCert != null ) - tmpCert.delete(); } } + + /** + * Call deployment script, passing along predefined key/cert/chain files. + */ + private boolean createFromFiles( File tmpKey, File tmpCert, File tmpChain ) + { + List<String> cmd; + int ret; + + cmd = new ArrayList<>( BASE_CMD ); + cmd.add( "--test" ); + cmd.add( tmpKey.getAbsolutePath() ); + cmd.add( tmpCert.getAbsolutePath() ); + ret = Exec.sync( 45, status, cmd.toArray( new String[ cmd.size() ] ) ); + if ( ret != 0 ) { + status.processStdOut( "Given key and certificate do not match, or have invalid format (exit code: " + ret + ")" ); + return false; + } + cmd = new ArrayList<>( BASE_CMD ); + if ( this.redirect ) { + cmd.add( "--redirect" ); + } + cmd.add( "--import" ); + cmd.add( tmpKey.getAbsolutePath() ); + cmd.add( tmpCert.getAbsolutePath() ); + if ( tmpChain != null ) { + cmd.add( tmpChain.getAbsolutePath() ); + } + ret = Exec.sync( 45, status, cmd.toArray( new String[ cmd.size() ] ) ); + if ( ret != 0 ) { + status.processStdOut( "import exited with code " + ret ); + return false; + } + return true; + } + + private boolean handleAcme() + { + if ( this.acmeMode.equals( "issue" ) ) { + return createWithAcme(); + } + if ( this.acmeMode.equals( "renew" ) ) { + return checkRenewAcme(); + } + if ( this.acmeMode.equals( "try-enable" ) ) { + return tryEnableAcme(); + } + status.processStdOut( "Invalid ACME mode: " + this.acmeMode ); + return false; + } + + /** + * Create a brand new certificate via ACME + */ + private boolean createWithAcme() + { + if ( Util.isEmpty( this.acmeMail ) ) { + status.processStdOut( "Invalid E-Mail provided" ); + return false; + } + if ( this.acmeDomains == null || this.acmeDomains.length == 0 ) { + status.processStdOut( "No domains provided" ); + return false; + } + List<String> cmd = new ArrayList<>( BASE_CMD ); + if ( this.redirect ) { + cmd.add( "--redirect" ); + } + if ( this.acmeWipeAll ) { + cmd.add( "--acme-wipe" ); + } + if ( this.acmeKeyId != null ) { + cmd.add( "--acme-key-id" ); + cmd.add( this.acmeKeyId ); + } + if ( this.acmeHmacKey != null ) { + cmd.add( "--acme-hmac-key" ); + cmd.add( this.acmeHmacKey ); + } + cmd.add( "--acme-issue" ); + cmd.add( this.acmeProvider ); + cmd.add( this.acmeMail ); + for ( String d : this.acmeDomains ) { + cmd.add( d ); + } + int ret = Exec.sync( 120, status, cmd.toArray( new String[ cmd.size() ] ) ); + if ( ret != 0 ) { + status.processStdOut( "acme issue exited with code " + ret ); + return false; + } + return true; + } + + /** + * Trigger a renew check and according renew if required. + */ + private boolean checkRenewAcme() + { + if ( this.acmeDomains == null || this.acmeDomains.length == 0 ) { + status.processStdOut( "No domain provided" ); + return false; + } + List<String> cmd = new ArrayList<>( BASE_CMD ); + if ( this.redirect ) { + cmd.add( "--redirect" ); + } + cmd.add( "--acme-renew" ); + cmd.add( this.acmeDomains[0] ); // Only needs primary domain to find config + int ret = Exec.sync( 120, status, cmd.toArray( new String[ cmd.size() ] ) ); + if ( ret != 0 ) { + status.processStdOut( "acme renew exited with code " + ret ); + return false; + } + return true; + } + + private boolean tryEnableAcme() + { + List<String> cmd = new ArrayList<>( BASE_CMD ); + if ( this.redirect ) { + cmd.add( "--redirect" ); + } + cmd.add( "--acme-try-enable" ); + int ret = Exec.sync( 10, status, cmd.toArray( new String[ cmd.size() ] ) ); + if ( ret != 0 ) { + status.processStdOut( "acme try-enable exited with code " + ret ); + return false; + } + return true; + } private boolean setRedirect() { - List<String> cmd = new ArrayList<>( baseCmd ); + List<String> cmd = new ArrayList<>( BASE_CMD ); cmd.add( "--redirect-only" ); if ( this.redirect ) { cmd.add( "--redirect" ); } int ret = Exec.sync( 10, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { - status.error = "set redirect exited with code " + ret; + status.processStdOut( "set redirect exited with code " + ret ); return false; } return true; @@ -144,11 +292,11 @@ public class LighttpdHttps extends AbstractTask private boolean disableHttps() { - List<String> cmd = new ArrayList<>( baseCmd ); + List<String> cmd = new ArrayList<>( BASE_CMD ); cmd.add( "--disable" ); int ret = Exec.sync( 10, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { - status.error = "disable exited with code " + ret; + status.processStdOut( "disable exited with code " + ret ); return false; } return true; @@ -157,10 +305,25 @@ public class LighttpdHttps extends AbstractTask /** * Output - contains additional status data of this task */ - @SuppressWarnings( "unused" ) - private static class Output + private static class Output implements ExecCallback { - protected String error = null; + protected StringBuilder error; + + @Override + public void processStdOut( String line ) + { + if ( error == null ) { + error = new StringBuilder(); + } + error.append( line ); + error.append( '\n' ); + } + + @Override + public void processStdErr( String line ) + { + processStdOut( line ); + } } } |