summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2024-10-08 16:25:56 +0200
committerSimon Rettberg2024-10-08 16:25:56 +0200
commit76dc961f86cc884b815aa28a436add1c128085fa (patch)
treed00b23243bd0a530d630144e468b014f39be51b7
parentAdd comment (diff)
downloadtmlite-bwlp-76dc961f86cc884b815aa28a436add1c128085fa.tar.gz
tmlite-bwlp-76dc961f86cc884b815aa28a436add1c128085fa.tar.xz
tmlite-bwlp-76dc961f86cc884b815aa28a436add1c128085fa.zip
[LighttpdHttps] Add support for ACME
-rwxr-xr-xscripts/install-https142
-rwxr-xr-xscripts/system-backup2
-rw-r--r--src/main/java/org/openslx/taskmanager/tasks/LighttpdHttps.java245
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 );
+ }
}
}