GUACAMOLE-1289: Merge refactor Duo and authentication flow

diff --git a/Dockerfile b/Dockerfile
index 29231f3..f85e040 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -61,6 +61,9 @@
 
 # Add configuration scripts
 COPY guacamole-docker/bin/ /opt/guacamole/bin/
+COPY guacamole-docker/build.d/ /opt/guacamole/build.d/
+COPY guacamole-docker/entrypoint.d/ /opt/guacamole/entrypoint.d/
+COPY guacamole-docker/environment/ /opt/guacamole/environment/
 
 # Copy source to container for sake of build
 COPY . "$BUILD_DIR"
@@ -68,12 +71,14 @@
 # Run the build itself
 RUN /opt/guacamole/bin/build-guacamole.sh "$BUILD_DIR" /opt/guacamole
 
+RUN rm -rf /opt/guacamole/build.d /opt/guacamole/bin/build-guacamole.sh
+
 # For the runtime image, we start with the official Tomcat distribution
 FROM tomcat:${TOMCAT_VERSION}-${TOMCAT_JRE}
 
-# Install XMLStarlet for server.xml alterations and unzip for LOGBACK_LEVEL case
+# Install XMLStarlet for server.xml alterations
 RUN apt-get update -qq \
-    && apt-get install -y xmlstarlet unzip\
+    && apt-get install -y xmlstarlet \
     && rm -rf /var/lib/apt/lists/* 
 
 # This is where the build artifacts go in the runtime image
@@ -91,6 +96,11 @@
 # Run with user guacamole
 USER guacamole
 
+# Environment variable defaults
+ENV BAN_ENABLED=true \
+    ENABLE_FILE_ENVIRONMENT_PROPERTIES=true \
+    GUACAMOLE_HOME=/etc/guacamole
+
 # Start Guacamole under Tomcat, listening on 0.0.0.0:8080
 EXPOSE 8080
-CMD ["/opt/guacamole/bin/start.sh" ]
+CMD ["/opt/guacamole/bin/entrypoint.sh" ]
diff --git a/guacamole-docker/bin/build-guacamole.sh b/guacamole-docker/bin/build-guacamole.sh
index 2fc6c95..595bd70 100755
--- a/guacamole-docker/bin/build-guacamole.sh
+++ b/guacamole-docker/bin/build-guacamole.sh
@@ -1,4 +1,4 @@
-#!/bin/sh -e
+#!/bin/bash -e
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -23,10 +23,15 @@
 ##
 ## Builds Guacamole, saving "guacamole.war" and all applicable extension .jars
 ## using the guacamole-client source contained within the given directory.
-## Extension files will be grouped by their associated type, with all MySQL
-## files being placed within the "mysql/" subdirectory of the destination, all
-## PostgreSQL files being placed within the "postgresql/" subdirectory of the
-## destination, etc.
+## Extension files will be grouped by their associated type, identical to
+## extracting the .tar.gz files included with each Guacamole release except
+## that version numbers are stripped from directory and .jar file names.
+##
+## The build process is split across multiple scripts within the
+## /opt/guacamole/build.d directory. Additional steps may be added to the
+## build process by adding .sh scripts to this directory. Any such scripts MUST
+## be shell scripts ending with a ".sh" extension and MUST be written for bash
+## (the shell used by this entrypoint).
 ##
 ## @param BUILD_DIR
 ##     The directory which currently contains the guacamole-client source and
@@ -39,164 +44,21 @@
 ##     extension type.
 ##
 
+##
+## The directory which currently contains the guacamole-client source and in
+## which the build should be performed.
+##
 BUILD_DIR="$1"
+
+##
+## The directory to save guacamole.war within, along with all extension .jars.
+## Note that this script will create extension-specific subdirectories within
+## this directory, and files will thus be grouped by extension type.
+##
 DESTINATION="$2"
 
-#
-# Create destination, if it does not yet exist
-#
+# Run all scripts within the "build.d" directory
+for SCRIPT in /opt/guacamole/build.d/*.sh; do
+    source "$SCRIPT"
+done
 
-mkdir -p "$DESTINATION"
-
-#
-# Build guacamole.war and all extensions
-#
-
-cd "$BUILD_DIR"
-
-#
-# Run the maven build, applying any arbitrary provided maven arguments.
-#
-
-mvn $MAVEN_ARGUMENTS package
-
-#
-# Copy guacamole.war to destination
-#
-
-cp guacamole/target/*.war "$DESTINATION/guacamole.war"
-
-#
-# Copy JDBC auth extensions and SQL scripts
-#
-
-tar -xzf extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/target/*.tar.gz \
-    -C "$DESTINATION"                                   \
-    --wildcards                                         \
-    --no-anchored                                       \
-    --strip-components=1                                \
-    "*.jar"                                             \
-    "*.sql"
-
-#
-# Download MySQL JDBC driver
-#
-
-echo "Downloading MySQL Connector/J ..."
-curl -L "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-j-$MYSQL_JDBC_VERSION.tar.gz" | \
-tar -xz                        \
-    -C "$DESTINATION/mysql/"   \
-    --wildcards                \
-    --no-anchored              \
-    --no-wildcards-match-slash \
-    --strip-components=1       \
-    "mysql-connector-*.jar"
-
-#
-# Download PostgreSQL JDBC driver
-#
-
-echo "Downloading PostgreSQL JDBC driver ..."
-curl -L "https://jdbc.postgresql.org/download/postgresql-$PGSQL_JDBC_VERSION.jar" \
-    > "$DESTINATION/postgresql/postgresql-$PGSQL_JDBC_VERSION.jar"
-
-#
-# Copy SSO auth extensions
-#
-
-tar -xzf extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/target/*.tar.gz \
-    -C "$DESTINATION"                                   \
-    --wildcards                                         \
-    --no-anchored                                       \
-    --strip-components=1                                \
-    "*.jar"
-
-#
-# Download SQL Server JDBC driver
-#
-
-echo "Downloading SQL Server JDBC driver ..."
-curl -L "https://github.com/microsoft/mssql-jdbc/releases/download/v$MSSQL_JDBC_VERSION/mssql-jdbc-$MSSQL_JDBC_VERSION.jre8.jar" \
-    > "$DESTINATION/sqlserver/mssql-jdbc-$MSSQL_JDBC_VERSION.jre8.jar"   \
-
-#
-# Copy LDAP auth extension and schema modifications
-#
-
-mkdir -p "$DESTINATION/ldap"
-tar -xzf extensions/guacamole-auth-ldap/target/*.tar.gz \
-    -C "$DESTINATION/ldap"                              \
-    --wildcards                                         \
-    --no-anchored                                       \
-    --xform="s#.*/##"                                   \
-    "*.jar"                                             \
-    "*.ldif"
-
-#
-# Copy Radius auth extension if it was build
-#
-
-if [ -f extensions/guacamole-auth-radius/target/guacamole-auth-radius*.jar ]; then
-    mkdir -p "$DESTINATION/radius"
-    cp extensions/guacamole-auth-radius/target/guacamole-auth-radius*.jar "$DESTINATION/radius"
-fi
-
-#
-# Copy TOTP auth extension if it was built
-#
-
-if [ -f extensions/guacamole-auth-totp/target/guacamole-auth-totp*.jar ]; then
-    mkdir -p "$DESTINATION/totp"
-    cp extensions/guacamole-auth-totp/target/guacamole-auth-totp*.jar "$DESTINATION/totp"
-fi
-
-#
-# Copy Duo auth extension if it was built
-#
-
-if [ -f extensions/guacamole-auth-duo/target/*.tar.gz ]; then
-    mkdir -p "$DESTINATION/duo"
-    tar -xzf extensions/guacamole-auth-duo/target/*.tar.gz \
-        -C "$DESTINATION/duo/"                             \
-        --wildcards                                        \
-        --no-anchored                                      \
-        --no-wildcards-match-slash                         \
-        --strip-components=1                               \
-        "*.jar"
-fi
-
-#
-# Copy header auth extension if it was built
-#
-
-if [ -f extensions/guacamole-auth-header/target/guacamole-auth-header*.jar ]; then
-    mkdir -p "$DESTINATION/header"
-    cp extensions/guacamole-auth-header/target/guacamole-auth-header*.jar "$DESTINATION/header"
-fi
-
-#
-# Copy json auth extension if it was built
-#
-
-if [ -f extensions/guacamole-auth-json/target/guacamole-auth-json*.jar ]; then
-    mkdir -p "$DESTINATION/json"
-    cp extensions/guacamole-auth-json/target/guacamole-auth-json*.jar "$DESTINATION/json"
-fi
-
-#
-# Copy automatic brute-force banning auth extension if it was built
-#
-
-if [ -f extensions/guacamole-auth-ban/target/guacamole-auth-ban*.jar ]; then
-    mkdir -p "$DESTINATION/ban"
-    cp extensions/guacamole-auth-ban/target/guacamole-auth-ban*.jar "$DESTINATION/ban"
-fi
-
-#
-# Copy history recording storage extension if it was built
-#
-
-if [ -f extensions/guacamole-history-recording-storage/target/guacamole-history-recording-storage*.jar ]; then
-    mkdir -p "$DESTINATION/recordings"
-    cp extensions/guacamole-history-recording-storage/target/guacamole-history-recording-storage*.jar "$DESTINATION/recordings"
-fi
diff --git a/guacamole-docker/bin/entrypoint.sh b/guacamole-docker/bin/entrypoint.sh
new file mode 100755
index 0000000..509332f
--- /dev/null
+++ b/guacamole-docker/bin/entrypoint.sh
@@ -0,0 +1,39 @@
+#!/bin/bash -e
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn entrypoint.sh
+##
+## (Re-)configures the Apache Guacamole web application based on the values of
+## environment variables, deploys the web application beneath a bundled copy of
+## Apache Tomcat, and starts Tomcat.
+##
+## The startup process is split across multiple scripts within the
+## /opt/guacamole/entrypoint.d directory. Additional steps may be added to the
+## startup process by adding .sh scripts to this directory. Any such scripts
+## MUST be shell scripts ending with a ".sh" extension and MUST be written for
+## bash (the shell used by this entrypoint).
+##
+
+# Run all scripts within the "entrypoint.d" directory
+for SCRIPT in /opt/guacamole/entrypoint.d/*.sh; do
+    source "$SCRIPT"
+done
+
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
deleted file mode 100755
index 4588c0e..0000000
--- a/guacamole-docker/bin/start.sh
+++ /dev/null
@@ -1,1251 +0,0 @@
-#!/bin/bash -e
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-##
-## @fn start.sh
-##
-## Automatically configures and starts Guacamole under Tomcat. Guacamole's
-## guacamole.properties file will be automatically generated based on the
-## linked database container (either MySQL, PostgreSQL or SQLServer) and the linked guacd
-## container. The Tomcat process will ultimately replace the process of this
-## script, running in the foreground until terminated.
-##
-
-GUACAMOLE_HOME_TEMPLATE="$GUACAMOLE_HOME"
-
-GUACAMOLE_HOME="$HOME/.guacamole"
-GUACAMOLE_EXT="$GUACAMOLE_HOME/extensions"
-GUACAMOLE_LIB="$GUACAMOLE_HOME/lib"
-GUACAMOLE_PROPERTIES="$GUACAMOLE_HOME/guacamole.properties"
-
-##
-## Sets the given property to the given value within guacamole.properties,
-## creating guacamole.properties first if necessary.
-##
-## @param NAME
-##     The name of the property to set.
-##
-## @param VALUE
-##     The value to set the property to.
-##
-set_property() {
-
-    NAME="$1"
-    VALUE="$2"
-
-    # Ensure guacamole.properties exists
-    if [ ! -e "$GUACAMOLE_PROPERTIES" ]; then
-        mkdir -p "$GUACAMOLE_HOME"
-        echo "# guacamole.properties - generated `date`" > "$GUACAMOLE_PROPERTIES"
-    fi
-
-    # Set property
-    echo "$NAME: $VALUE" >> "$GUACAMOLE_PROPERTIES"
-
-}
-
-##
-## Sets the given property to the given value within guacamole.properties only
-## if a value is provided, creating guacamole.properties first if necessary.
-##
-## @param NAME
-##     The name of the property to set.
-##
-## @param VALUE
-##     The value to set the property to, if any. If omitted or empty, the
-##     property will not be set.
-##
-set_optional_property() {
-
-    NAME="$1"
-    VALUE="$2"
-
-    # Set the property only if a value is provided
-    if [ -n "$VALUE" ]; then
-        set_property "$NAME" "$VALUE"
-    fi
-
-}
-
-# Print error message regarding missing required variables for MySQL authentication
-mysql_missing_vars() {
-   cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using a MySQL database, you must provide each of the following
-environment variables or their corresponding Docker secrets by appending _FILE
-to the environment variable, and setting the value to the path of the
-corresponding secret:
-
-    MYSQL_USER         The user to authenticate as when connecting to
-                       MySQL.
-
-    MYSQL_PASSWORD     The password to use when authenticating with MySQL as
-                       MYSQL_USER.
-
-    MYSQL_DATABASE     The name of the MySQL database to use for Guacamole
-                       authentication.
-END
-    exit 1;
-}
-
-
-##
-## Adds properties to guacamole.properties which select the MySQL
-## authentication provider, and configure it to connect to the linked MySQL
-## container. If a MySQL database is explicitly specified using the
-## MYSQL_HOSTNAME and MYSQL_PORT environment variables, that will be used
-## instead of a linked container.
-##
-associate_mysql() {
-
-    # Use linked container if specified
-    if [ -n "$MYSQL_NAME" ]; then
-        MYSQL_HOSTNAME="$MYSQL_PORT_3306_TCP_ADDR"
-        MYSQL_PORT="$MYSQL_PORT_3306_TCP_PORT"
-    fi
-
-    # Use default port if none specified
-    MYSQL_PORT="${MYSQL_PORT-3306}"
-
-    # Verify required connection information is present
-    if [ -z "$MYSQL_HOSTNAME" -o -z "$MYSQL_PORT" ]; then
-        cat <<END
-FATAL: Missing MYSQL_HOSTNAME or "mysql" link.
--------------------------------------------------------------------------------
-If using a MySQL database, you must either:
-
-(a) Explicitly link that container with the link named "mysql".
-
-(b) If not using a Docker container for MySQL, explicitly specify the TCP
-    connection to your database using the following environment variables:
-
-    MYSQL_HOSTNAME     The hostname or IP address of the MySQL server. If not
-                       using a MySQL Docker container and corresponding link,
-                       this environment variable is *REQUIRED*.
-
-    MYSQL_PORT         The port on which the MySQL server is listening for TCP
-                       connections. This environment variable is option. If
-                       omitted, the standard MySQL port of 3306 will be used.
-END
-        exit 1;
-    fi
-
-
-    # Verify that the required Docker secrets are present, else, default to their normal environment variables
-    if [ -n "$MYSQL_USER_FILE" ]; then
-        set_property "mysql-username" "`cat "$MYSQL_USER_FILE"`"
-    elif [ -n "$MYSQL_USER" ]; then
-        set_property "mysql-username" "$MYSQL_USER"
-    else
-        mysql_missing_vars
-        exit 1;
-    fi
-
-    if [ -n "$MYSQL_PASSWORD_FILE" ]; then
-        set_property "mysql-password" "`cat "$MYSQL_PASSWORD_FILE"`"
-    elif [ -n "$MYSQL_PASSWORD" ]; then
-        set_property "mysql-password" "$MYSQL_PASSWORD"
-    else
-        mysql_missing_vars
-        exit 1;
-    fi
-
-    if [ -n "$MYSQL_DATABASE_FILE" ]; then
-        set_property "mysql-database" "`cat "$MYSQL_DATABASE_FILE"`"
-    elif [ -n "$MYSQL_DATABASE" ]; then
-        set_property "mysql-database" "$MYSQL_DATABASE"
-    else
-        mysql_missing_vars
-        exit 1;
-    fi
-
-    # Update config file
-    set_property "mysql-hostname" "$MYSQL_HOSTNAME"
-    set_property "mysql-port"     "$MYSQL_PORT"
-
-    set_optional_property               \
-        "mysql-absolute-max-connections" \
-        "$MYSQL_ABSOLUTE_MAX_CONNECTIONS"
-
-    set_optional_property               \
-        "mysql-default-max-connections" \
-        "$MYSQL_DEFAULT_MAX_CONNECTIONS"
-
-    set_optional_property                     \
-        "mysql-default-max-group-connections" \
-        "$MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS"
-
-    set_optional_property                        \
-        "mysql-default-max-connections-per-user" \
-        "$MYSQL_DEFAULT_MAX_CONNECTIONS_PER_USER"
-
-    set_optional_property                              \
-        "mysql-default-max-group-connections-per-user" \
-        "$MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER"
-
-    set_optional_property     \
-        "mysql-user-required" \
-        "$MYSQL_USER_REQUIRED"
-
-    set_optional_property \
-        "mysql-ssl-mode"  \
-        "$MYSQL_SSL_MODE"
-
-    set_optional_property        \
-        "mysql-ssl-trust-store"  \
-        "$MYSQL_SSL_TRUST_STORE"
-
-    # For SSL trust store password, check secrets, first, then standard env variable
-    if [ -n "$MYSQL_SSL_TRUST_PASSWORD_FILE" ]; then
-        set_property "mysql-ssl-trust-password" "`cat "$MYSQL_SSL_TRUST_PASSWORD_FILE"`"
-    elif [ -n "$MYSQL_SSL_TRUST_PASSWORD" ]; then
-        set_property "mysql-ssl-trust-password" "$MYSQL_SSL_TRUST_PASSWORD"
-    fi
-
-    set_optional_property         \
-        "mysql-ssl-client-store"  \
-        "$MYSQL_SSL_CLIENT_STORE"
-
-    # For SSL trust store password, check secrets, first, then standard env variable
-    if [ -n "$MYSQL_SSL_CLIENT_PASSWORD_FILE" ]; then
-        set_property "mysql-ssl-client-password" "`cat "$MYSQL_SSL_CLIENT_PASSWORD_FILE"`"
-    elif [ -n "$MYSQL_SSL_CLIENT_PASSWORD" ]; then
-        set_property "mysql-ssl-client-password" "$MYSQL_SSL_CLIENT_PASSWORD"
-    fi
-
-    set_optional_property             \
-        "mysql-auto-create-accounts"  \
-        "$MYSQL_AUTO_CREATE_ACCOUNTS"
-
-    # Add required .jar files to GUACAMOLE_LIB and GUACAMOLE_EXT
-    ln -s /opt/guacamole/mysql/mysql-connector-*.jar "$GUACAMOLE_LIB"
-    ln -s /opt/guacamole/mysql/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-
-}
-
-# Print error message regarding missing required variables for PostgreSQL authentication
-postgresql_missing_vars() {
-    cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using a PostgreSQL database, you must provide each of the following
-environment variables or their corresponding Docker secrets by appending _FILE
-to the environment variable, and setting the value to the path of the
-corresponding secret:
-
-    POSTGRESQL_USER      The user to authenticate as when connecting to
-                         PostgreSQL.
-
-    POSTGRESQL_PASSWORD  The password to use when authenticating with PostgreSQL
-                         as POSTGRESQL_USER.
-
-    POSTGRESQL_DATABASE  The name of the PostgreSQL database to use for Guacamole
-                         authentication.
-END
-    exit 1;
-}
-
-## Provide backward compatibility on POSTGRES_* environment variables
-## In case of new deployment, please use POSTGRESQL_* equivalent variables.
-for VAR_BASE in \
-    HOSTNAME PORT \
-    DATABASE USER PASSWORD \
-    DATABASE_FILE USER_FILE PASSWORD_FILE \
-    ABSOLUTE_MAX_CONNECTIONS DEFAULT_MAX_CONNECTIONS \
-    DEFAULT_MAX_GROUP_CONNECTIONS DEFAULT_MAX_CONNECTIONS_PER_USER \
-    DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER \
-    DEFAULT_STATEMENT_TIMEOUT SOCKET_TIMEOUT \
-    USER_REQUIRED \
-    SSL_KEY_PASSWORD_FILE SSL_KEY_PASSWORD; do
-
-        OLD_VAR="POSTGRES_$VAR_BASE"
-        NEW_VAR="POSTGRESQL_$VAR_BASE"
-
-        if [ -n "${!OLD_VAR}" ]; then
-            printf -v "$NEW_VAR" "%s" "${!OLD_VAR}"
-            echo "WARNING: ${OLD_VAR} detected, please use ${NEW_VAR} for further deployments."
-        fi
-
-done
-
-##
-## Adds properties to guacamole.properties which select the PostgreSQL
-## authentication provider, and configure it to connect to the linked
-## PostgreSQL container. If a PostgreSQL database is explicitly specified using
-## the POSTGRESQL_HOSTNAME and POSTGRESQL_PORT environment variables, that will be
-## used instead of a linked container.
-##
-associate_postgresql() {
-
-    # Use linked container if specified
-    if [ -n "$POSTGRES_NAME" ]; then
-        POSTGRESQL_HOSTNAME="$POSTGRES_PORT_5432_TCP_ADDR"
-        POSTGRESQL_PORT="$POSTGRES_PORT_5432_TCP_PORT"
-    fi
-
-    # Use default port if none specified
-    POSTGRESQL_PORT="${POSTGRESQL_PORT-5432}"
-
-    # Verify required connection information is present
-    if [ -z "$POSTGRESQL_HOSTNAME" -o -z "$POSTGRESQL_PORT" ]; then
-        cat <<END
-FATAL: Missing POSTGRESQL_HOSTNAME or "postgres" link.
--------------------------------------------------------------------------------
-If using a PostgreSQL database, you must either:
-
-(a) Explicitly link that container with the link named "postgres".
-
-(b) If not using a Docker container for PostgreSQL, explicitly specify the TCP
-    connection to your database using the following environment variables:
-
-    POSTGRESQL_HOSTNAME  The hostname or IP address of the PostgreSQL server. If
-                         not using a PostgreSQL Docker container and
-                         corresponding link, this environment variable is
-                         *REQUIRED*.
-
-    POSTGRESQL_PORT      The port on which the PostgreSQL server is listening for
-                         TCP connections. This environment variable is option. If
-                         omitted, the standard PostgreSQL port of 5432 will be
-                         used.
-END
-        exit 1;
-    fi
-
-    # Verify that the required Docker secrets are present, else, default to their normal environment variables
-    if [ -n "$POSTGRESQL_USER_FILE" ]; then
-        set_property "postgresql-username" "`cat "$POSTGRESQL_USER_FILE"`"
-    elif [ -n "$POSTGRESQL_USER" ]; then
-        set_property "postgresql-username" "$POSTGRESQL_USER"
-    else
-        postgresql_missing_vars
-        exit 1;
-    fi
-
-    if [ -n "$POSTGRESQL_PASSWORD_FILE" ]; then
-        set_property "postgresql-password" "`cat "$POSTGRESQL_PASSWORD_FILE"`"
-    elif [ -n "$POSTGRESQL_PASSWORD" ]; then
-        set_property "postgresql-password" "$POSTGRESQL_PASSWORD"
-    else
-        postgresql_missing_vars
-        exit 1;
-    fi
-
-    if [ -n "$POSTGRESQL_DATABASE_FILE" ]; then
-        set_property "postgresql-database" "`cat "$POSTGRESQL_DATABASE_FILE"`"
-    elif [ -n "$POSTGRESQL_DATABASE" ]; then
-        set_property "postgresql-database" "$POSTGRESQL_DATABASE"
-    else
-        postgresql_missing_vars
-        exit 1;
-    fi
-
-    # Update config file
-    set_property "postgresql-hostname" "$POSTGRESQL_HOSTNAME"
-    set_property "postgresql-port"     "$POSTGRESQL_PORT"
-
-    set_optional_property               \
-        "postgresql-absolute-max-connections" \
-        "$POSTGRESQL_ABSOLUTE_MAX_CONNECTIONS"
-
-    set_optional_property                    \
-        "postgresql-default-max-connections" \
-        "$POSTGRESQL_DEFAULT_MAX_CONNECTIONS"
-
-    set_optional_property                          \
-        "postgresql-default-max-group-connections" \
-        "$POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS"
-
-    set_optional_property                             \
-        "postgresql-default-max-connections-per-user" \
-        "$POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER"
-
-    set_optional_property                                   \
-        "postgresql-default-max-group-connections-per-user" \
-        "$POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER"
-
-    set_optional_property                      \
-        "postgresql-default-statement-timeout" \
-        "$POSTGRESQL_DEFAULT_STATEMENT_TIMEOUT"
-
-    set_optional_property          \
-        "postgresql-user-required" \
-        "$POSTGRESQL_USER_REQUIRED"
-
-    set_optional_property           \
-        "postgresql-socket-timeout" \
-        "$POSTGRESQL_SOCKET_TIMEOUT"
-
-    set_optional_property      \
-        "postgresql-ssl-mode"  \
-        "$POSTGRESQL_SSL_MODE"
-
-    set_optional_property           \
-        "postgresql-ssl-cert-file"  \
-        "$POSTGRESQL_SSL_CERT_FILE"
-
-    set_optional_property          \
-        "postgresql-ssl-key-file"  \
-        "$POSTGRESQL_SSL_KEY_FILE"
-
-    set_optional_property                \
-        "postgresql-ssl-root-cert-file"  \
-        "$POSTGRESQL_SSL_ROOT_CERT_FILE"
-
-    # For SSL key password, check secrets, first, then standard env variable
-    if [ -n "$POSTGRESQL_SSL_KEY_PASSWORD_FILE" ]; then
-        set_property "postgresql-ssl-key-password" "`cat "$POSTGRESQL_SSL_KEY_PASSWORD_FILE"`"
-    elif [ -n "$POSTGRESQL_SSL_KEY_PASSWORD" ]; then
-        set_property "postgresql-ssl-key-password" "$POSTGRESQL_SSL_KEY_PASSWORD"
-    fi
-
-    set_optional_property                  \
-        "postgresql-auto-create-accounts"  \
-        "$POSTGRESQL_AUTO_CREATE_ACCOUNTS"
-
-    # Add required .jar files to GUACAMOLE_LIB and GUACAMOLE_EXT
-    ln -s /opt/guacamole/postgresql/postgresql-*.jar "$GUACAMOLE_LIB"
-    ln -s /opt/guacamole/postgresql/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-
-}
-
-# Print error message regarding missing required variables for SQLServer authentication
-sqlserver_missing_vars() {
-    cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using a SQLServer database, you must provide each of the following
-environment variables:
-
-    SQLSERVER_USER     The user to authenticate as when connecting to
-                       SQLServer.
-
-    SQLSERVER_PASSWORD The password to use when authenticating with SQLServer
-                       as SQLSERVER_USER.
-
-    SQLSERVER_DATABASE The name of the SQLServer database to use for Guacamole
-                       authentication.
-
-Alternatively, if you want to store database credentials using Docker secrets,
-set the path of the corresponding secrets in the following three variables:
-
-    SQLSERVER_DATABASE_FILE   The path of the docker secret containing the name
-                              of database to use for Guacamole authentication.
-
-    SQLSERVER_USER_FILE       The path of the docker secret containing the name of
-                              the user that Guacamole will use to connect to SQLServer.
-
-    SQLSERVER_PASSWORD_FILE   The path of the docker secret containing the
-                              password that Guacamole will provide when connecting to
-                              SQLServer as SQLSERVER_USER.
-
-END
-    exit 1;
-}
-
-##
-## Adds properties to guacamole.properties which select the SQLServer
-## authentication provider, and configure it to connect to the linked
-## SQLServer container. If a SQLServer database is explicitly specified using
-## the SQLSERVER_HOSTNAME and SQLSERVER_PORT environment variables, that will
-## be used instead of a linked container.
-##
-associate_sqlserver() {
-
-    # Use linked container if specified
-    if [ -n "$SQLSERVER_NAME" ]; then
-        SQLSERVER_HOSTNAME="$SQLSERVER_PORT_1433_TCP_ADDR"
-        SQLSERVER_PORT="$SQLSERVER_PORT_1433_TCP_PORT"
-    fi
-
-    # Use default port if none specified
-    SQLSERVER_PORT="${SQLSERVER_PORT-1433}"
-
-    # Verify required connection information is present
-    if [ -z "$SQLSERVER_HOSTNAME" -o -z "$SQLSERVER_PORT" ]; then
-        cat <<END
-FATAL: Missing SQLSERVER_HOSTNAME or "sqlserver" link.
--------------------------------------------------------------------------------
-If using a SQLServer database, you must either:
-
-(a) Explicitly link that container with the link named "sqlserver".
-
-(b) If not using a Docker container for SQLServer, explicitly specify the TCP
-    connection to your database using the following environment variables:
-
-    SQLSERVER_HOSTNAME The hostname or IP address of the SQLServer server. If
-                       not using a SQLServer Docker container and
-                       corresponding link, this environment variable is
-                       *REQUIRED*.
-
-    SQLSERVER_PORT     The port on which the SQLServer server is listening for
-                       TCP connections. This environment variable is option. If
-                       omitted, the standard SQLServer port of 1433 will be
-                       used.
-END
-        exit 1;
-    fi
-
-    # Verify that the required Docker secrets are present, else, default to their normal environment variables
-    if [ -n "$SQLSERVER_USER_FILE" ]; then
-        set_property "sqlserver-username" "`cat "$SQLSERVER_USER_FILE"`"
-    elif [ -n "$SQLSERVER_USER" ]; then
-        set_property "sqlserver-username" "$SQLSERVER_USER"
-    else
-        sqlserver_missing_vars
-        exit 1;
-    fi
-
-    if [ -n "$SQLSERVER_PASSWORD_FILE" ]; then
-        set_property "sqlserver-password" "`cat "$SQLSERVER_PASSWORD_FILE"`"
-    elif [ -n "$SQLSERVER_PASSWORD" ]; then
-        set_property "sqlserver-password" "$SQLSERVER_PASSWORD"
-    else
-        sqlserver_missing_vars
-        exit 1;
-    fi
-
-    if [ -n "$SQLSERVER_DATABASE_FILE" ]; then
-        set_property "sqlserver-database" "`cat "$SQLSERVER_DATABASE_FILE"`"
-    elif [ -n "$SQLSERVER_DATABASE" ]; then
-        set_property "sqlserver-database" "$SQLSERVER_DATABASE"
-    else
-        sqlserver_missing_vars
-        exit 1;
-    fi
-
-    # Update config file
-    set_property "sqlserver-hostname" "$SQLSERVER_HOSTNAME"
-    set_property "sqlserver-port"     "$SQLSERVER_PORT"
-    set_property "sqlserver-driver"   "microsoft2005"
-
-    set_optional_property               \
-        "sqlserver-absolute-max-connections" \
-        "$SQLSERVER_ABSOLUTE_MAX_CONNECTIONS"
-
-    set_optional_property                    \
-        "sqlserver-default-max-connections" \
-        "$SQLSERVER_DEFAULT_MAX_CONNECTIONS"
-
-    set_optional_property                          \
-        "sqlserver-default-max-group-connections" \
-        "$SQLSERVER_DEFAULT_MAX_GROUP_CONNECTIONS"
-
-    set_optional_property                             \
-        "sqlserver-default-max-connections-per-user" \
-        "$SQLSERVER_DEFAULT_MAX_CONNECTIONS_PER_USER"
-
-    set_optional_property                                   \
-        "sqlserver-default-max-group-connections-per-user" \
-        "$SQLSERVER_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER"
-
-    set_optional_property          \
-        "sqlserver-user-required" \
-        "$SQLSERVER_USER_REQUIRED"
-
-    set_optional_property                  \
-        "sqlserver-auto-create-accounts"  \
-        "$SQLSERVER_AUTO_CREATE_ACCOUNTS"
-
-    set_optional_property      \
-        "sqlserver-instance"  \
-        "$SQLSERVER_INSTANCE"
-
-    # Add required .jar files to GUACAMOLE_LIB and GUACAMOLE_EXT
-    ln -s /opt/guacamole/sqlserver/mssql-jdbc-*.jar "$GUACAMOLE_LIB"
-    ln -s /opt/guacamole/sqlserver/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-
-}
-
-##
-## Adds properties to guacamole.properties which select the LDAP
-## authentication provider, and configure it to connect to the specified LDAP
-## directory.
-##
-associate_ldap() {
-
-    # Verify required parameters are present
-    if [ -z "$LDAP_HOSTNAME" -o -z "$LDAP_USER_BASE_DN" ]; then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using an LDAP directory, you must provide each of the following environment
-variables:
-
-    LDAP_HOSTNAME      The hostname or IP address of your LDAP server.
-
-    LDAP_USER_BASE_DN  The base DN under which all Guacamole users will be
-                       located. Absolutely all Guacamole users that will
-                       authenticate via LDAP must exist within the subtree of
-                       this DN.
-END
-        exit 1;
-    fi
-
-    # Update config file
-    set_property          "ldap-hostname"                   "$LDAP_HOSTNAME"
-    set_property          "ldap-user-base-dn"               "$LDAP_USER_BASE_DN"
-
-    set_optional_property "ldap-port"                       "$LDAP_PORT"
-    set_optional_property "ldap-encryption-method"          "$LDAP_ENCRYPTION_METHOD"
-    set_optional_property "ldap-max-search-results"         "$LDAP_MAX_SEARCH_RESULTS"
-    set_optional_property "ldap-search-bind-dn"             "$LDAP_SEARCH_BIND_DN"
-    set_optional_property "ldap-user-attributes"            "$LDAP_USER_ATTRIBUTES"
-    set_optional_property "ldap-search-bind-password"       "$LDAP_SEARCH_BIND_PASSWORD"
-    set_optional_property "ldap-username-attribute"         "$LDAP_USERNAME_ATTRIBUTE"
-    set_optional_property "ldap-member-attribute"           "$LDAP_MEMBER_ATTRIBUTE"
-    set_optional_property "ldap-user-search-filter"         "$LDAP_USER_SEARCH_FILTER"
-    set_optional_property "ldap-config-base-dn"             "$LDAP_CONFIG_BASE_DN"
-    set_optional_property "ldap-group-base-dn"              "$LDAP_GROUP_BASE_DN"
-    set_optional_property "ldap-group-search-filter"        "$LDAP_GROUP_SEARCH_FILTER"
-    set_optional_property "ldap-member-attribute-type"      "$LDAP_MEMBER_ATTRIBUTE_TYPE"
-    set_optional_property "ldap-group-name-attribute"       "$LDAP_GROUP_NAME_ATTRIBUTE"
-    set_optional_property "ldap-dereference-aliases"        "$LDAP_DEREFERENCE_ALIASES"
-    set_optional_property "ldap-follow-referrals"           "$LDAP_FOLLOW_REFERRALS"
-    set_optional_property "ldap-max-referral-hops"          "$LDAP_MAX_REFERRAL_HOPS"
-    set_optional_property "ldap-operation-timeout"          "$LDAP_OPERATION_TIMEOUT"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/ldap/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-
-}
-
-##
-## Adds properties to guacamole.properties which select the LDAP
-## authentication provider, and configure it to connect to the specified LDAP
-## directory.
-##
-associate_radius() {
-
-    # Verify required parameters are present
-    if [ -z "$RADIUS_SHARED_SECRET" -o -z "$RADIUS_AUTH_PROTOCOL" ]; then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using RADIUS server, you must provide each of the following environment
-variables:
-
-    RADIUS_SHARED_SECRET   The shared secret to use when talking to the
-                           RADIUS server.
-
-    RADIUS_AUTH_PROTOCOL   The authentication protocol to use when talking
-                           to the RADIUS server.
-                           Supported values are:
-                             pap, chap, mschapv1, mschapv2, eap-md5,
-                             eap-tls and eap-ttls.
-END
-        exit 1;
-    fi
-
-    # Verify provided files do exist and are readable
-    if [ -n "$RADIUS_KEY_FILE" -a ! -r "$RADIUS_KEY_FILE" ]; then
-       cat <<END
-FATAL: Provided file RADIUS_KEY_FILE=$RADIUS_KEY_FILE does not exist
-       or is not readable!
--------------------------------------------------------------------------------
-If you provide key or CA files you need to mount those into the container and
-make sure they are readable for the user in the container.
-END
-        exit 1;
-    fi
-    if [ -n "$RADIUS_CA_FILE" -a ! -r "$RADIUS_CA_FILE" ]; then
-       cat <<END
-FATAL: Provided file RADIUS_CA_FILE=$RADIUS_CA_FILE does not exist
-       or is not readable!
--------------------------------------------------------------------------------
-If you provide key or CA files you need to mount those into the container and
-make sure they are readable for the user in the container.
-END
-        exit 1;
-    fi
-    if [ "$RADIUS_AUTH_PROTOCOL" = "eap-ttls" -a -z "$RADIUS_EAP_TTLS_INNER_PROTOCOL" ]; then
-       cat <<END
-FATAL: Authentication protocol "eap-ttls" specified but
-       RADIUS_EAP_TTLS_INNER_PROTOCOL is not set!
--------------------------------------------------------------------------------
-When EAP-TTLS is used, this parameter specifies the inner (tunneled)
-protocol to use talking to the RADIUS server.
-END
-        exit 1;
-    fi
-
-    # Update config file
-    set_optional_property "radius-hostname"                 "$RADIUS_HOSTNAME"
-    set_optional_property "radius-auth-port"                "$RADIUS_AUTH_PORT"
-    set_property          "radius-shared-secret"            "$RADIUS_SHARED_SECRET"
-    set_property          "radius-auth-protocol"            "$RADIUS_AUTH_PROTOCOL"
-    set_optional_property "radius-key-file"                 "$RADIUS_KEY_FILE"
-    set_optional_property "radius-key-type"                 "$RADIUS_KEY_TYPE"
-    set_optional_property "radius-key-password"             "$RADIUS_KEY_PASSWORD"
-    set_optional_property "radius-ca-file"                  "$RADIUS_CA_FILE"
-    set_optional_property "radius-ca-type"                  "$RADIUS_CA_TYPE"
-    set_optional_property "radius-ca-password"              "$RADIUS_CA_PASSWORD"
-    set_optional_property "radius-trust-all"                "$RADIUS_TRUST_ALL"
-    set_optional_property "radius-retries"                  "$RADIUS_RETRIES"
-    set_optional_property "radius-timeout"                  "$RADIUS_TIMEOUT"
-    set_optional_property "radius-eap-ttls-inner-protocol"  "$RADIUS_EAP_TTLS_INNER_PROTOCOL"
-    set_optional_property "radius-nas-ip"                   "$RADIUS_NAS_IP"
-
-    set_optional_property \
-       "radius-eap-ttls-inner-protocol" \
-       "$RADIUS_EAP_TTLS_INNER_PROTOCOL"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/radius/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-}
-
-## Adds properties to guacamole.properties which select the OPENID
-## authentication provider, and configure it to connect to the specified OPENID
-## provider.
-##
-associate_openid() {
-
-    # Verify required parameters are present
-    if [ -z "$OPENID_AUTHORIZATION_ENDPOINT" ] || \
-       [ -z "$OPENID_JWKS_ENDPOINT" ]          || \
-       [ -z "$OPENID_ISSUER" ]                 || \
-       [ -z "$OPENID_CLIENT_ID" ]              || \
-       [ -z "$OPENID_REDIRECT_URI" ]
-    then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using an openid authentication, you must provide each of the following
-environment variables:
-
-    OPENID_AUTHORIZATION_ENDPOINT   The authorization endpoint (URI) of the OpenID service.
-
-    OPENID_JWKS_ENDPOINT            The endpoint (URI) of the JWKS service which defines
-                                    how received ID tokens (JSON Web Tokens or JWTs)
-                                    shall be validated.
-
-    OPENID_ISSUER                   The issuer to expect for all received ID tokens.
-
-    OPENID_CLIENT_ID                The OpenID client ID which should be submitted
-                                    to the OpenID service when necessary.
-                                    This value is typically provided to you by the OpenID
-                                    service when OpenID credentials are generated for your application.
-
-    OPENID_REDIRECT_URI             The URI that should be submitted to the OpenID service such that
-                                    they can redirect the authenticated user back to Guacamole after
-                                    the authentication process is complete. This must be the full URL
-                                    that a user would enter into their browser to access Guacamole.
-END
-        exit 1;
-    fi
-
-    # Update config file
-    set_property          "openid-authorization-endpoint"    "$OPENID_AUTHORIZATION_ENDPOINT"
-    set_property          "openid-jwks-endpoint"             "$OPENID_JWKS_ENDPOINT"
-    set_property          "openid-issuer"                    "$OPENID_ISSUER"
-    set_property          "openid-client-id"                 "$OPENID_CLIENT_ID"
-    set_property          "openid-redirect-uri"              "$OPENID_REDIRECT_URI"
-    set_optional_property "openid-username-claim-type"       "$OPENID_USERNAME_CLAIM_TYPE"
-    set_optional_property "openid-groups-claim-type"         "$OPENID_GROUPS_CLAIM_TYPE"
-    set_optional_property "openid-scope"                     "$OPENID_SCOPE"
-    set_optional_property "openid-allowed-clock-skew"        "$OPENID_ALLOWED_CLOCK_SKEW"
-    set_optional_property "openid-max-token-validity"        "$OPENID_MAX_TOKEN_VALIDITY"
-    set_optional_property "openid-max-nonce-validity"        "$OPENID_MAX_NONCE_VALIDITY"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    # "1-{}" make it sorted as a first provider (only authentication)
-    # so it can work together with the database providers (authorization)
-    find /opt/guacamole/openid/ -name "*.jar" | awk -F/ '{print $NF}' | \
-    xargs -I '{}' ln -s "/opt/guacamole/openid/{}" "${GUACAMOLE_EXT}/1-{}"
-
-}
-
-##
-## Adds properties to guacamole.properties which select the SAML
-## authentication provider, and configure it to connect to the specified SAML
-## provider.
-##
-
-associate_saml() {
-
-    # Verify required parameters are present
-    if [ -z "$SAML_IDP_METADATA_URL" ] && \
-       [ -z "$SAML_ENTITY_ID" -o -z "$SAML_CALLBACK_URL" -o -z "$SAML_IDP_URL" ]
-    then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using a SAML authentication, you must provide either SAML_IDP_METADATA_URL
-or SAML_IDP_URL,  SAML_ENTITY_ID and SAML_CALLBACK_URL environment variables:
-
-    SAML_IDP_METADATA_URL   The URI of the XML metadata file that from the SAML Identity
-                            Provider that contains all of the information the SAML
-                            extension needs in order to know how to authenticate with
-                            the IdP. This URI can either be a remote server (e.g. https://)
-                            or a local file on the filesystem (e.g. file://).
-
-    SAML_IDP_URL            The URL of the Identity Provider (IdP), which the user
-                            will be redirected to in order to authenticate.
-
-    SAML_ENTITY_ID          The entity ID of the Guacamole SAML client, which is
-                            generally the URL of the Guacamole server.
-
-    SAML_CALLBACK_URL       The URL that the IdP will use once authentication has
-                            succeeded to return to the Guacamole web application and
-                            provide the authentication details to the SAML extension.
-END
-        exit 1;
-    fi
-
-    # Update config file
-    set_optional_property "saml-idp-metadata-url"            "$SAML_IDP_METADATA_URL"
-    set_optional_property "saml-idp-url"                     "$SAML_IDP_URL"
-    set_optional_property "saml-entity-id"                   "$SAML_ENTITY_ID"
-    set_optional_property "saml-callback-url"                "$SAML_CALLBACK_URL"
-    set_optional_property "saml-strict"                      "$SAML_STRICT"
-    set_optional_property "saml-debug"                       "$SAML_DEBUG"
-    set_optional_property "saml-compress-request"            "$SAML_COMPRESS_REQUEST"
-    set_optional_property "saml-compress-response"           "$SAML_COMPRESS_RESPONSE"
-    set_optional_property "saml-group-attribute"             "$SAML_GROUP_ATTRIBUTE"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    # "1-{}" make it sorted as a first provider (only authentication)
-    # so it can work together with the database providers (authorization)
-    find /opt/guacamole/saml/ -name "*.jar" | awk -F/ '{print $NF}' | \
-    xargs -I '{}' ln -s "/opt/guacamole/saml/{}" "${GUACAMOLE_EXT}/1-{}"
-
-}
-
-##
-## Adds properties to guacamole.properties which configure the TOTP two-factor
-## authentication mechanism.
-##
-associate_totp() {
-    # Update config file
-    set_optional_property "totp-issuer"    "$TOTP_ISSUER"
-    set_optional_property "totp-digits"    "$TOTP_DIGITS"
-    set_optional_property "totp-period"    "$TOTP_PERIOD"
-    set_optional_property "totp-mode"      "$TOTP_MODE"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/totp/guacamole-auth-*.jar   "$GUACAMOLE_EXT"
-}
-
-##
-## Adds properties to guacamole.properties which configure the Duo two-factor
-## authentication service. Checks to see if all variables are defined
-##
-associate_duo() {
-    # Verify required parameters are present
-    if [ -z "$DUO_CLIENT_ID" ]            || \
-       [ -z "$DUO_CLIENT_SECRET" ]        || \
-       [ -z "$DUO_REDIRECT_URI" ]
-    then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using the Duo authentication extension, you must provide each of the
-following environment variables:
-
-    DUO_API_HOSTNAME        The hostname of the Duo API endpoint.
-
-    DUO_CLIENT_ID           The client id (or integration key) provided for Guacamole by Duo.
-
-    DUO_CLIENT_SECRET       The secret key provided for Guacamole by Duo.
-
-    DUO_REDIRECT_URI        The URI to redirect back to upon successful authentication.
-END
-        exit 1;
-    fi
-
-    # Update config file
-    set_property "duo-api-hostname"                 "$DUO_API_HOSTNAME"
-    set_property "duo-client-id"                    "$DUO_CLIENT_ID"
-    set_property "duo-client-secret"                "$DUO_CLIENT_SECRET"
-    set_property "duo-redirect-uri"                 "$DUO_REDIRECT_URI"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/duo/guacamole-auth-*.jar   "$GUACAMOLE_EXT"
-}
-
-##
-## Adds properties to guacamole.properties which configure the header
-## authentication provider.
-##
-associate_header() {
-    # Update config file
-    set_optional_property "http-auth-header"         "$HTTP_AUTH_HEADER"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/header/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-}
-
-##
-## Adds properties to guacamole.properties witch configure the CAS
-## authentication service.
-##
-associate_cas() {
-    # Verify required parameters are present
-    if [ -z "$CAS_AUTHORIZATION_ENDPOINT" ] || \
-       [ -z "$CAS_REDIRECT_URI" ]
-    then
-        cat <<END
-FATAL: Missing required environment variables
------------------------------------------------------------------------------------
-If using the CAS authentication extension, you must provide each of the
-following environment variables:
-
-    CAS_AUTHORIZATION_ENDPOINT      The URL of the CAS authentication server.
-
-    CAS_REDIRECT_URI                The URI to redirect back to upon successful authentication.
-
-END
-        exit 1;
-    fi
-
-    # Update config file
-    set_property            "cas-authorization-endpoint"       "$CAS_AUTHORIZATION_ENDPOINT"
-    set_property            "cas-redirect-uri"                 "$CAS_REDIRECT_URI"
-    set_optional_property   "cas-clearpass-key"                "$CAS_CLEARPASS_KEY"
-    set_optional_property   "cas-group-attribute"              "$CAS_GROUP_ATTRIBUTE"
-    set_optional_property   "cas-group-format"                 "$CAS_GROUP_FORMAT"
-    set_optional_property   "cas-group-ldap-base-dn"           "$CAS_GROUP_LDAP_BASE_DN"
-    set_optional_property   "cas-group-ldap-attribute"         "$CAS_GROUP_LDAP_ATTRIBUTE"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/cas/guacamole-auth-*.jar   "$GUACAMOLE_EXT"
-}
-
-##
-## Adds properties to guacamole.properties which configure the json
-## authentication provider.
-##
-associate_json() {
-    # Update config file
-    set_property          "json-secret-key"        "$JSON_SECRET_KEY"
-    set_optional_property "json-trusted-networks"  "$JSON_TRUSTED_NETWORKS"
-
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/json/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-}
-
-##  
-## Adds properties to guacamole.properties which configure the recording
-## storage extension.
-##  
-associate_recordings() {
-    # Update config file
-    set_property "recording-search-path" "$RECORDING_SEARCH_PATH"
-    
-    # Add required .jar files to GUACAMOLE_EXT
-    ln -s /opt/guacamole/recordings/guacamole-history-recording-storage-*.jar "$GUACAMOLE_EXT"
-}
-
-##
-## Sets up Tomcat's remote IP valve that allows gathering the remote IP
-## from headers set by a remote proxy
-## Upstream documentation: https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html
-##
-enable_remote_ip_valve() {
-    # Add <Valve> element
-    xmlstarlet edit --inplace \
-        --insert '/Server/Service/Engine/Host/*' --type elem -n Valve \
-        --insert '/Server/Service/Engine/Host/Valve[not(@className)]' --type attr -n className -v org.apache.catalina.valves.RemoteIpValve \
-        $CATALINA_BASE/conf/server.xml
-
-    # Allowed IPs
-    if [ -z "$PROXY_ALLOWED_IPS_REGEX" ]; then
-        echo "Using default Tomcat allowed IPs regex"
-    else
-        xmlstarlet edit --inplace \
-            --insert '/Server/Service/Engine/Host/Valve[@className="org.apache.catalina.valves.RemoteIpValve"]' \
-            --type attr -n internalProxies -v "$PROXY_ALLOWED_IPS_REGEX" \
-            $CATALINA_BASE/conf/server.xml
-    fi
-
-    # X-Forwarded-For
-    if [ -z "$PROXY_IP_HEADER" ]; then
-        echo "Using default Tomcat proxy IP header"
-    else
-        xmlstarlet edit --inplace \
-            --insert "/Server/Service/Engine/Host/Valve[@className='org.apache.catalina.valves.RemoteIpValve']" \
-            --type attr -n remoteIpHeader -v "$PROXY_IP_HEADER" \
-            $CATALINA_BASE/conf/server.xml
-    fi
-
-    # X-Forwarded-Proto
-    if [ -z "$PROXY_PROTOCOL_HEADER" ]; then
-        echo "Using default Tomcat proxy protocol header"
-    else
-        xmlstarlet edit --inplace \
-            --insert "/Server/Service/Engine/Host/Valve[@className='org.apache.catalina.valves.RemoteIpValve']" \
-            --type attr -n protocolHeader -v "$PROXY_PROTOCOL_HEADER" \
-            $CATALINA_BASE/conf/server.xml
-    fi
-
-    # X-Forwarded-By
-    if [ -z "$PROXY_BY_HEADER" ]; then
-        echo "Using default Tomcat proxy forwarded by header"
-    else
-        xmlstarlet edit --inplace \
-            --insert "/Server/Service/Engine/Host/Valve[@className='org.apache.catalina.valves.RemoteIpValve']" \
-            --type attr -n remoteIpProxiesHeader -v "$PROXY_BY_HEADER" \
-            $CATALINA_BASE/conf/server.xml
-    fi
-}
-
-##
-## Adds api-session-timeout to guacamole.properties
-##
-associate_apisessiontimeout() {
-    set_optional_property "api-session-timeout" "$API_SESSION_TIMEOUT"
-}
-
-##
-## Starts Guacamole under Tomcat, replacing the current process with the
-## Tomcat process. As the current process will be replaced, this MUST be the
-## last function run within the script.
-##
-start_guacamole() {
-
-    # User-only writable CATALINA_BASE
-    export CATALINA_BASE=$HOME/tomcat
-    for dir in logs temp webapps work; do
-        mkdir -p $CATALINA_BASE/$dir
-    done
-    cp -R /usr/local/tomcat/conf $CATALINA_BASE
-
-    # Set up Tomcat RemoteIPValve
-    if [ "$REMOTE_IP_VALVE_ENABLED" = "true" ]; then
-        enable_remote_ip_valve
-    fi
-
-    # Install webapp
-    ln -sf /opt/guacamole/guacamole.war $CATALINA_BASE/webapps/${WEBAPP_CONTEXT:-guacamole}.war
-
-    # Start tomcat
-    cd /usr/local/tomcat
-    exec catalina.sh run
-
-}
-
-#
-# Start with a fresh GUACAMOLE_HOME
-#
-
-rm -Rf "$GUACAMOLE_HOME"
-
-#
-# Copy contents of provided GUACAMOLE_HOME template, if any
-#
-
-if [ -n "$GUACAMOLE_HOME_TEMPLATE" ]; then
-    cp -a "$GUACAMOLE_HOME_TEMPLATE/." "$GUACAMOLE_HOME/"
-fi
-
-#
-# Create and define Guacamole lib and extensions directories
-#
-
-mkdir -p "$GUACAMOLE_EXT"
-mkdir -p "$GUACAMOLE_LIB"
-
-#
-# Point to associated guacd
-#
-
-# Use linked container for guacd if specified
-if [ -n "$GUACD_NAME" ]; then
-    GUACD_HOSTNAME="$GUACD_PORT_4822_TCP_ADDR"
-    GUACD_PORT="$GUACD_PORT_4822_TCP_PORT"
-fi
-
-# Use default guacd port if none specified
-GUACD_PORT="${GUACD_PORT-4822}"
-
-# Verify required guacd connection information is present
-if [ -z "$GUACD_HOSTNAME" -o -z "$GUACD_PORT" ]; then
-    cat <<END
-FATAL: Missing GUACD_HOSTNAME or "guacd" link.
--------------------------------------------------------------------------------
-Every Guacamole instance needs a corresponding copy of guacd running. To
-provide this, you must either:
-
-(a) Explicitly link that container with the link named "guacd".
-
-(b) If not using a Docker container for guacd, explicitly specify the TCP
-    connection information using the following environment variables:
-
-GUACD_HOSTNAME     The hostname or IP address of guacd. If not using a guacd
-                   Docker container and corresponding link, this environment
-                   variable is *REQUIRED*.
-
-GUACD_PORT         The port on which guacd is listening for TCP connections.
-                   This environment variable is optional. If omitted, the
-                   standard guacd port of 4822 will be used.
-END
-    exit 1;
-fi
-
-# Update config file
-set_property "guacd-hostname" "$GUACD_HOSTNAME"
-set_property "guacd-port"     "$GUACD_PORT"
-
-# A comma-separated list of the identifiers of authentication providers that
-# should be allowed to fail internally without aborting the authentication process
-set_optional_property "skip-if-unavailable"     "$SKIP_IF_UNAVAILABLE"
-
-
-#
-# Track which authentication backends are installed
-#
-
-INSTALLED_AUTH=""
-
-# Use MySQL if database specified
-if [ -n "$MYSQL_DATABASE" -o -n "$MYSQL_DATABASE_FILE" ]; then
-    associate_mysql
-    INSTALLED_AUTH="$INSTALLED_AUTH mysql"
-fi
-
-# Use PostgreSQL if database specified
-if [ -n "$POSTGRESQL_DATABASE" -o -n "$POSTGRESQL_DATABASE_FILE" ]; then
-    associate_postgresql
-    INSTALLED_AUTH="$INSTALLED_AUTH postgresql"
-fi
-
-# Use SQLServer if database specified
-if [ -n "$SQLSERVER_DATABASE" -o -n "$SQLSERVER_DATABASE_FILE" ]; then
-    associate_sqlserver
-    INSTALLED_AUTH="$INSTALLED_AUTH sqlserver"
-fi
-
-# Use LDAP directory if specified
-if [ -n "$LDAP_HOSTNAME" ]; then
-    associate_ldap
-    INSTALLED_AUTH="$INSTALLED_AUTH ldap"
-fi
-
-# Use RADIUS server if specified
-if [ -n "$RADIUS_SHARED_SECRET" ]; then
-    associate_radius
-    INSTALLED_AUTH="$INSTALLED_AUTH radius"
-fi
-
-# Use OPENID if specified
-if [ -n "$OPENID_AUTHORIZATION_ENDPOINT" ]; then
-    associate_openid
-    INSTALLED_AUTH="$INSTALLED_AUTH openid"
-fi
-
-# Use SAML if specified
-if [ -n "$SAML_IDP_METADATA_URL" ] || [ -n "$SAML_ENTITY_ID" -a -n "$SAML_CALLBACK_URL" ]; then
-    associate_saml
-    INSTALLED_AUTH="$INSTALLED_AUTH saml"
-fi
-
-# Use TOTP if specified.
-if [ "$TOTP_ENABLED" = "true" ]; then
-    associate_totp
-fi
-
-# Use Duo if specified.
-if [ -n "$DUO_API_HOSTNAME" ]; then
-    associate_duo
-fi
-
-# Use header if specified.
-if [ "$HEADER_ENABLED" = "true" ]; then
-    associate_header
-fi
-
-# Use CAS if specified.
-if [ -n "$CAS_AUTHORIZATION_ENDPOINT" ]; then
-    associate_cas
-fi
-
-# Use json-auth if specified.
-if [ -n "$JSON_SECRET_KEY" ]; then
-    associate_json
-    INSTALLED_AUTH="$INSTALLED_AUTH json"
-fi
-
-# Add in the history recording storage extension if configured
-if [ -n "$RECORDING_SEARCH_PATH" ]; then
-    associate_recordings
-fi
-
-#
-# Validate that at least one authentication backend is installed
-#
-
-if [ -z "$INSTALLED_AUTH" -a -z "$GUACAMOLE_HOME_TEMPLATE" ]; then
-    cat <<END
-FATAL: No authentication configured
--------------------------------------------------------------------------------
-The Guacamole Docker container needs at least one authentication mechanism in
-order to function, such as a MySQL database, PostgreSQL database, SQLServer
-database, LDAP directory or RADIUS server. Please specify at least the
-MYSQL_DATABASE or POSTGRESQL_DATABASE or SQLSERVER_DATABASE environment variables,
-or check Guacamole's Docker documentation regarding configuring LDAP and/or
-custom extensions.
-END
-    exit 1;
-fi
-
-# Set extension priority if specified
-set_optional_property "extension-priority" "$EXTENSION_PRIORITY"
-
-# Use api-session-timeout if specified.
-if [ -n "$API_SESSION_TIMEOUT" ]; then
-    associate_apisessiontimeout
-fi
-
-# Maximum number of bytes to accept within the entity body of any particular HTTP request
-set_optional_property "api-max-request-size" "$API_MAX_REQUEST_SIZE"
-
-# A comma-separated list of language keys to allow as display language 
-# choices within the Guacamole interface
-set_optional_property "allowed-languages" "$ALLOWED_LANGUAGES"
-
-# If set to “true”, Guacamole will first evaluate its environment to obtain the value
-# for any given configuration property, before using a value specified in 
-# guacamole.properties or falling back to a default value
-set_optional_property "enable-environment-properties" "$ENABLE_ENVIRONMENT_PROPERTIES"
-
-
-# Apply any overrides for default address ban behavior
-set_optional_property "ban-address-duration" "$BAN_ADDRESS_DURATION"
-set_optional_property "ban-max-addresses" "$BAN_MAX_ADDRESSES"
-set_optional_property "ban-max-invalid-attempts" "$BAN_MAX_INVALID_ATTEMPTS"
-
-# Always load guacamole-auth-ban extension (automatic banning can be disabled
-# through seting BAN_ADDRESS_DURATION to 0). As guacamole-auth-ban performs
-# its banning by handling a pre-authentication event, it is guaranteed to
-# perform its checks before all other auth processing and load order does not
-# matter.
-ln -s /opt/guacamole/ban/guacamole-auth-*.jar "$GUACAMOLE_EXT"
-
-# Set logback level if specified
-if [ -n "$LOGBACK_LEVEL" ]; then
-    unzip -o -j /opt/guacamole/guacamole.war WEB-INF/classes/logback.xml -d $GUACAMOLE_HOME
-    sed -i "s/level=\"info\"/level=\"$LOGBACK_LEVEL\"/" $GUACAMOLE_HOME/logback.xml
-fi
-
-#
-# Finally start Guacamole (under Tomcat)
-#
-
-start_guacamole
diff --git a/guacamole-docker/build.d/000-build-and-install-guacamole.sh b/guacamole-docker/build.d/000-build-and-install-guacamole.sh
new file mode 100644
index 0000000..8c9c772
--- /dev/null
+++ b/guacamole-docker/build.d/000-build-and-install-guacamole.sh
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+## 
+## @fn 010-build-and-install-guacamole.sh
+##
+## Builds the Guacamole web application and all main extensions, installing the
+## resulting binaries to standard locations within the Docker image. After the
+## build and install process, the resulting binaries can be found beneath:
+##
+## /opt/guacamole/webapp:
+##   The web application, "guacamole.war".
+##
+## /opt/guacamole/extensions:
+##   All extensions, each within their own subdirectory and identical to the
+##   result of extracting a released .tar.gz except that version numbers of been
+##   stripped.
+##
+
+#
+# Build guacamole.war and all extensions, applying any provided Maven build
+# arguments
+#
+
+cd "$BUILD_DIR"
+mvn $MAVEN_ARGUMENTS package
+
+#
+# Copy built web application (guacamole.war) to destination location
+#
+
+mkdir -p "$DESTINATION/webapp"
+cp guacamole/target/*.war "$DESTINATION/webapp/guacamole.war"
+
+#
+# Extract all extensions to destination location, stripping version number
+# suffix from .jar files and top-level directory name
+#
+
+mkdir -p "$DESTINATION/extensions"
+find extensions/ -path "**/target/*.tar.gz" -exec tar -xzf "{}" \
+    -C "$DESTINATION/extensions"                                \
+    --xform='s#^\([^/]*\)-[0-9]\+\.[0-9]\+\.[0-9]\+#\1#g'       \
+    --xform='s#-[0-9]\+\.[0-9]\+\.[0-9]\+\(\.jar$\)#\1#g'       \
+    ";"
+
diff --git a/guacamole-docker/build.d/010-map-guacamole-extensions.sh b/guacamole-docker/build.d/010-map-guacamole-extensions.sh
new file mode 100644
index 0000000..de12262
--- /dev/null
+++ b/guacamole-docker/build.d/010-map-guacamole-extensions.sh
@@ -0,0 +1,118 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn 020-map-guacamole-extensions.sh
+##
+## Maps all installed Guacamole extensions (built in a previous step) to their
+## corresponding environment variable prefixes, adding symbolic links so that
+## the changes to the contents of GUACAMOLE_HOME can be easily made by the
+## container's entrypoint based on which environment variables are set, without
+## requiring that the entrypoint be specifically aware of all supported
+## environment variables.
+##
+
+##
+## Reads a mapping of Guacamole extension to environment variable prefix from
+## STDIN, creating a hierarchy of directories and symbolic links on the
+## filesystem that can be easily consumed by the container's entrypoint later.
+##
+## Each mapping consists of a single line with two values separated by
+## whitespace, where the first (leftmost) value is the path to the directory
+## containing the extension .jar file (relative to /opt/guacamole/extensions)
+## and the second (rightmost) value is the environment variable prefix used by
+## that extension. For readability, periods may be used in lieu of spaces.
+##
+## After mapping has occurred, the resulting mappings are located beneath
+## /opt/guacamole/environment. They consist of directories named after the
+## provided environment variable prefixes, where the contents of those
+## directories are subsets of the contents of GUACAMOLE_HOME that would need to
+## be added to the actual GUACAMOLE_HOME to enable that extension.
+##
+map_extensions() {
+
+    # Read through each provided path/prefix mapping pair
+    mkdir -p "$DESTINATION/environment"
+    tr . ' ' | while read -r EXT_PATH VAR_PREFIX; do
+
+        # Add mappings only for extensions that were actually built as part of
+        # the build process (some extensions, like the RADIUS support, will
+        # only be built if specific build arguments are provided)
+        if [ -d "$DESTINATION/extensions/$EXT_PATH/" ]; then
+            echo "Mapped: $EXT_PATH -> $VAR_PREFIX"
+            mkdir -p "$DESTINATION/environment/$VAR_PREFIX/extensions"
+            ln -s "$DESTINATION/extensions/$EXT_PATH"/*.jar "$DESTINATION/environment/$VAR_PREFIX/extensions/"
+        else
+            echo "Skipped: $EXT_PATH (not built)"
+        fi
+
+    done
+
+}
+
+#
+# This section is a mapping of all bundled extensions to their corresponding
+# variable prefixes. Each line consists of a whitespace-separated pair of
+# extension path (the relative directory containing the .jar file) to that
+# extension's variable prefix. For readability, a period may be used in lieu of
+# a space.
+#
+# NOTES:
+#
+# (1) The actual variables used by each extension are not determined here, but
+# rather by the transformation of their configuration properties to variables
+# ("lowercase-with-dashes" to "UPPERCASE_WITH_UNDERSCORES"). The variable
+# prefixes listed here should be chosen to match the prefixes resulting from
+# that transformation of the extensions' properties.
+#
+# (2) The paths on the left side of this mapping are the paths of the extension
+# .jar files relative to the "/opt/guacamole/extensions" directory used by the
+# container to store extensions prior to use. They are identical to the paths
+# used by the distribution .tar.gz files provided with each Guacamole release,
+# except that the version numbers have been stripped from the top-level path.
+#
+# (3) The script processing this file uses these prefixes to define and process
+# an additional "ENABLED" variable (ie: "BAN_ENABLED", "TOTP_ENABLED", etc.)
+# that can be used to enable/disable an extension entirely regardless of the
+# presence/absence of other variables with the prefix. This allows extensions
+# that need no configuration to be easily enabled. It also allows extensions
+# that already have configuration present to be easily disabled without
+# requiring that all other configuration be removed.
+#
+map_extensions <<'EOF'
+    guacamole-auth-ban..........................BAN_
+    guacamole-auth-duo..........................DUO_
+    guacamole-auth-header.......................HTTP_AUTH_
+    guacamole-auth-jdbc/mysql...................MYSQL_
+    guacamole-auth-jdbc/postgresql..............POSTGRESQL_
+    guacamole-auth-jdbc/sqlserver...............SQLSERVER_
+    guacamole-auth-json.........................JSON_
+    guacamole-auth-ldap.........................LDAP_
+    guacamole-auth-quickconnect.................QUICKCONNECT_
+    guacamole-auth-radius.......................RADIUS_
+    guacamole-auth-sso/cas......................CAS_
+    guacamole-auth-sso/openid...................OPENID_
+    guacamole-auth-sso/saml.....................SAML_
+    guacamole-auth-sso/ssl......................SSL_
+    guacamole-auth-totp.........................TOTP_
+    guacamole-display-statistics................DISPLAY_STATISTICS_
+    guacamole-history-recording-storage.........RECORDING_
+    guacamole-vault/ksm.........................KSM_
+EOF
+
diff --git a/guacamole-docker/build.d/020-download-drivers.sh b/guacamole-docker/build.d/020-download-drivers.sh
new file mode 100644
index 0000000..6613dc3
--- /dev/null
+++ b/guacamole-docker/build.d/020-download-drivers.sh
@@ -0,0 +1,99 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn 030-download-drivers.sh
+##
+## Downloads all JDBC drivers required by the various supported databases. Each
+## downloaded driver is stored beneath /opt/guacamole/drivers, with symbolic
+## links added to the mappings beneath /opt/guacamole/environment to ensure any
+## required drivers are added to GUACAMOLE_HOME if necessary to support a
+## requested database.
+##
+
+##
+## Downloads the JDBC driver at the given URL, storing the driver's .jar file
+## under the given name and environment variable prefix. The downloaded .jar
+## file is stored such that it is pulled into GUACAMOLE_HOME automatically if
+## environment variables with that prefix are used.
+##
+## If the URL is for a .tar.gz file and not a .jar file, the .jar will be
+## automatically extracted from the .tar.gz as it is downloaded.
+##
+## @param VAR_PREFIX
+##     The environment variable prefix used by the extension that requires the
+##     driver.
+##
+## @param URL
+##     The URL that the driver should be downloaded from.
+##
+## @param DEST_JAR
+##     The filename to assign to the downloaded .jar file. This is mainly
+##     needed to ensure that the drivers bundled with the image have names that
+##     are predictable and reliable enough that they can be consumed by
+##     third-party use of this image.
+##
+download_driver() {
+
+    local VAR_PREFIX="$1"
+    local URL="$2"
+    local DEST_JAR="$3"
+
+    # Ensure primary destination path for .jar file exists
+    local DEST_PATH="$DESTINATION/drivers/"
+    mkdir -p "$DEST_PATH"
+
+    # Download requested .jar file, extracting from .tar.gz if necessary
+    if [[ "$URL" == *.tar.gz ]]; then
+        curl -L "$URL" | tar -xz       \
+            --wildcards                \
+            --no-anchored              \
+            --no-wildcards-match-slash \
+            --to-stdout                \
+            "*.jar" > "$DEST_PATH/$DEST_JAR"
+    else
+        curl -L "$URL" > "$DEST_PATH/$DEST_JAR"
+    fi
+
+    # Add any required link to ensure the .jar file is loaded along with the
+    # extension that requires it
+    mkdir -p "$DESTINATION/environment/$VAR_PREFIX/lib"
+    ln -s "$DEST_PATH/$DEST_JAR" "$DESTINATION/environment/$VAR_PREFIX/lib/"
+
+}
+
+#
+# Download and link any required JDBC drivers
+#
+
+# MySQL JDBC driver
+download_driver "MYSQL_" \
+    "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-j-$MYSQL_JDBC_VERSION.tar.gz" \
+    "mysql-jdbc.jar"
+
+# PostgreSQL JDBC driver
+download_driver "POSTGRESQL_" \
+    "https://jdbc.postgresql.org/download/postgresql-$PGSQL_JDBC_VERSION.jar" \
+    "postgresql-jdbc.jar"
+
+# SQL Server JDBC driver
+download_driver "SQLSERVER_" \
+    "https://github.com/microsoft/mssql-jdbc/releases/download/v$MSSQL_JDBC_VERSION/mssql-jdbc-$MSSQL_JDBC_VERSION.jre8.jar" \
+    "mssql-jdbc.jar"
+
diff --git a/guacamole-docker/build.d/999-verify-sanity.sh b/guacamole-docker/build.d/999-verify-sanity.sh
new file mode 100644
index 0000000..48707da
--- /dev/null
+++ b/guacamole-docker/build.d/999-verify-sanity.sh
@@ -0,0 +1,47 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn 999-verify-sanity.sh
+##
+## Performs sanity checks on the results of the build that verify the image
+## contains everything it is expected to contain, including all built
+## extensions. If symbolic links were not correctly constructed, or some built
+## extensions were not mapped to environment variable prefixes, this script
+## will log errors and fail the build.
+##
+
+# Perform basic sanity checks that the symbolic links used to associated
+# environment variables with extensions/libraries have been correctly created,
+# bailing out if any problems are found.
+(
+
+    # Search for any broken symbolic links intended to map files for
+    # environment variables
+    find "$DESTINATION/environment/" -xtype l | sed 's/^/Broken link: /'
+
+    # Search for extensions that have not been mapped to any environment
+    # variables at all
+    comm -23 \
+        <(find "$DESTINATION/extensions/" -name "*.jar" -exec realpath "{}" ";" | sort -u) \
+        <(find "$DESTINATION/environment/" -path "**/extensions/*.jar" -exec realpath "{}" ";" | sort -u) \
+        | sed 's/^/Unmapped extension: /'
+
+) | sed 's/^/ERROR: /' | (! grep .) >&2 || exit 1
+
diff --git a/guacamole-docker/entrypoint.d/000-migrate-legacy-variables.sh b/guacamole-docker/entrypoint.d/000-migrate-legacy-variables.sh
new file mode 100644
index 0000000..cb56c4c
--- /dev/null
+++ b/guacamole-docker/entrypoint.d/000-migrate-legacy-variables.sh
@@ -0,0 +1,116 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn 000-migrate-legacy-variables.sh
+##
+## Checks for usage of any environment variables that were formerly supported
+## but are now deprecated, warning when any deprecated variables are
+## encountered.  Until support for a deprecated variable is entirely removed,
+## the value provided for the deprecated variable is automatically assigned to
+## the currently-supported variable.
+##
+
+##
+## Checks for usage of the given deprecated environment variable, automatically
+## assigning its value to the given currently-supported environment variable.
+## If usage of the deprecated variable is found, a warning is printed to
+## STDERR.
+##
+## @param LEGACY_VAR_NAME
+##     The name of the environment variable that's deprecated.
+##
+## @param CURRENT_VAR_NAME
+##     The name of the environment variable that is currently supported and
+##     replaces the deprecated variable.
+##
+deprecate_variable() {
+
+    local LEGACY_VAR_NAME="$1"
+    local CURRENT_VAR_NAME="$2"
+
+    if [ -n "${!LEGACY_VAR_NAME}" ]; then
+        echo "WARNING: The \"$LEGACY_VAR_NAME\" environment variable has been deprecated in favor of \"$CURRENT_VAR_NAME\". Please migrate your configuration when possible, as support for the older name may be removed in future releases." >&2
+        export "$CURRENT_VAR_NAME"="${!LEGACY_VAR_NAME}"
+    fi
+
+}
+
+##
+## Checks for usage of any environment variables using the given deprecated
+## prefix, automatically assigning their values to corresponding environment
+## variables having the given currently-supported prefix. If usage of the
+## deprecated prefix is found, a warning is printed to STDERR.
+##
+## @param LEGACY_VAR_PREFIX
+##     The environment variable prefix that's deprecated.
+##
+## @param CURRENT_VAR_PREFIX
+##     The environment variable prefix that is currently supported and
+##     replaces the deprecated variable prefix.
+##
+deprecate_variable_prefix() {
+
+    local LEGACY_VAR_PREFIX="$1"
+    local CURRENT_VAR_PREFIX="$2"
+
+    local LEGACY_VAR_NAME
+    local CURRENT_VAR_NAME
+    local HAS_LEGACY_VARIABLES=0
+
+    # Automatically reassign all "POSTGRES_*" variables to "POSTGRESQL_*"
+    while read -r LEGACY_VAR_NAME; do
+        HAS_LEGACY_VARIABLES=1
+        CURRENT_VAR_NAME="$CURRENT_VAR_PREFIX${LEGACY_VAR_NAME#$LEGACY_VAR_PREFIX}"
+        export "$CURRENT_VAR_NAME"="${!LEGACY_VAR_NAME}"
+        unset "$LEGACY_VAR_NAME"
+    done < <(awk 'BEGIN{for(v in ENVIRON) print v}' | grep "^$LEGACY_VAR_PREFIX")
+
+    if [ "$HAS_LEGACY_VARIABLES" = "1" ]; then
+        echo "WARNING: The \"$LEGACY_VAR_PREFIX\" prefix for environment variables has been deprecated in favor of the \"$CURRENT_VAR_PREFIX\" prefix. Please migrate your configuration when possible, as support for the older prefix may be removed in future releases." >&2
+        export "$CURRENT_VAR_NAME"="$LEGACY_VAR_NAME"
+    fi
+
+}
+
+# The old "*_USER" style for configuring the user account to be used to access
+# the database is being replaced with "*_USERNAME" such that all environment
+# variables exactly correspond to the names of configuration properties from
+# guacamole.properties.
+deprecate_variable "MYSQL_USER"     "MYSQL_USERNAME"
+deprecate_variable "POSTGRES_USER"  "POSTGRESQL_USERNAME"
+deprecate_variable "SQLSERVER_USER" "SQLSERVER_USERNAME"
+
+# The old "POSTGRES_" prefix for configuring usage of PostgreSQL is being
+# replaced with "POSTGRESQL_" such that all environment variables exactly
+# correspond to the names of configuration properties from
+# guacamole.properties.
+deprecate_variable_prefix "POSTGRES_" "POSTGRESQL_"
+
+# The old "PROXY_*" names for attributes supported by RemoteIpValve are being
+# replaced with "REMOTE_IP_VALVE_*" attributes that more closely and
+# predictably match their attribute names
+deprecate_variable "PROXY_ALLOWED_IPS_REGEX" "REMOTE_IP_VALVE_INTERNAL_PROXIES"
+deprecate_variable "PROXY_IP_HEADER"         "REMOTE_IP_VALVE_REMOTE_IP_HEADER"
+deprecate_variable "PROXY_PROTOCOL_HEADER"   "REMOTE_IP_VALVE_PROTOCOL_HEADER"
+# NOTE: PROXY_BY_HEADER never worked as there is no "remoteIpProxiesHeader" attribute for RemoteIpValve
+
+# The old "LOGBACK_LEVEL" environment variable has been replaced with
+# "LOG_LEVEL" for consistency with the guacd image
+deprecate_variable "LOGBACK_LEVEL" "LOG_LEVEL"
diff --git a/guacamole-docker/entrypoint.d/100-generate-guacamole-home.sh b/guacamole-docker/entrypoint.d/100-generate-guacamole-home.sh
new file mode 100644
index 0000000..1db4d60
--- /dev/null
+++ b/guacamole-docker/entrypoint.d/100-generate-guacamole-home.sh
@@ -0,0 +1,111 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn 010-generate-guacamole-home.sh
+##
+## Automatically generates a temporary, skeleton GUACAMOLE_HOME to be used for
+## this run of the container. GUACAMOLE_HOMEs from previous runs are
+## automatically deleted prior to creating the new skeleton. A
+## randomly-generated temporary directory is used instead of a standard
+## directory like "/etc/guacamole" to allow users to use "/etc/guacamole" as a
+## basis for their own configuration.
+##
+
+##
+## The directory to copy/link over as a basis for the GUACAMOLE_HOME actually
+## used by the Guacamole web application. Any configuration generated by this
+## container will be overlaid on top of this configuration. To achieve the
+## overlay, symbolic links will be created for all files inside and beneath
+## this directory. Only the guacamole.properties file will be copied instead of
+## using symbolic links (to ensure property generation performed by the
+## container does not potentially modify an external file).
+##
+GUACAMOLE_HOME_TEMPLATE="$GUACAMOLE_HOME"
+
+##
+## Tests whether a given property is set within the guacamole.properties file
+## in GUACAMOLE_HOME.
+##
+## @param PROPERTY_NAME
+##     The name of the property to check.
+##
+## @returns
+##     Zero if the given property is set to any value within
+##     guacamole.properties, non-zero otherwise.
+##
+is_property_set() {
+    local PROPERTY_NAME="$1"
+    grep "^[[:space:]]*$PROPERTY_NAME\>" "$GUACAMOLE_HOME/guacamole.properties" &> /dev/null
+}
+
+#
+# Start with a fresh GUACAMOLE_HOME
+#
+
+rm -rf /tmp/guacamole-home.*
+GUACAMOLE_HOME="`mktemp -p /tmp -d guacamole-home.XXXXXXXXXX`"
+mkdir -p "$GUACAMOLE_HOME/"{lib,extensions}
+
+cat > "$GUACAMOLE_HOME/guacamole.properties" <<EOF
+# guacamole.properties - generated `date`
+EOF
+
+#
+# Copy contents of provided GUACAMOLE_HOME template, if any
+#
+
+if [ -e "$GUACAMOLE_HOME_TEMPLATE" ]; then
+
+    # Create links for any libraries provided in the template GUACAMOLE_HOME
+    find "$GUACAMOLE_HOME_TEMPLATE/lib" -mindepth 1 -maxdepth 1 \
+        -exec ln -sv "{}" "$GUACAMOLE_HOME/lib/" ";"
+
+    # Create links for any extensions provided in the template GUACAMOLE_HOME
+    find "$GUACAMOLE_HOME_TEMPLATE/extensions" -mindepth 1 -maxdepth 1 \
+        -exec ln -sv "{}" "$GUACAMOLE_HOME/extensions/" ";"
+
+    # Create links for all other files directly within the template
+    # GUACAMOLE_HOME
+    find "$GUACAMOLE_HOME_TEMPLATE" -mindepth 1 -maxdepth 1 \
+        -name guacamole.properties -o -name lib -o -name extensions -prune \
+        -o -exec ln -sv "{}" "$GUACAMOLE_HOME/" ";"
+
+    # Add any properties provided within template GUACAMOLE_HOME
+    if [ -e "$GUACAMOLE_HOME_TEMPLATE/guacamole.properties" ]; then
+        cat "$GUACAMOLE_HOME_TEMPLATE/guacamole.properties" >> "$GUACAMOLE_HOME/guacamole.properties"
+    fi
+
+fi
+
+# Enable reading of properties directly from environment variables unless
+# overridden
+if ! is_property_set "enable-environment-properties"; then
+    cat >> "$GUACAMOLE_HOME/guacamole.properties" <<'EOF'
+#
+# NOTE: The following was automatically added by the container entrypoint to
+# allow all Guacamole configuration properties to be automatically read from
+# environment variables. If this is not desired, you can override this behavior
+# by specifying the "enable-environment-properties" variable yourself in your
+# own guacamole.properties file.
+#
+enable-environment-properties: true
+EOF
+fi
+
diff --git a/guacamole-docker/entrypoint.d/500-generate-tomcat-catalina-base.sh b/guacamole-docker/entrypoint.d/500-generate-tomcat-catalina-base.sh
new file mode 100644
index 0000000..cd86951
--- /dev/null
+++ b/guacamole-docker/entrypoint.d/500-generate-tomcat-catalina-base.sh
@@ -0,0 +1,50 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## 500-generate-tomcat-catalina-base.sh
+##
+## Automcatically generates a fresh, temporary CATALINA_BASE for Apache Tomcat.
+## This allows Tomcat to run as a reduced-privilege user, and allows its
+## configuration to be dynamically generated by the container entrypoint at
+## startup.
+##
+
+#
+# Start with a fresh CATALINA_BASE
+#
+
+rm -rf /tmp/catalina-base.*
+export CATALINA_BASE="`mktemp -p /tmp -d catalina-base.XXXXXXXXXX`"
+
+# User-only writable CATALINA_BASE
+for dir in logs temp webapps work; do
+    mkdir -p $CATALINA_BASE/$dir
+done
+cp -R /usr/local/tomcat/conf $CATALINA_BASE
+
+cat >> "$CATALINA_BASE/conf/catalina.properties" <<EOF
+
+# Point Guacamole at automatically-generated, temporary GUACAMOLE_HOME
+guacamole.home=$GUACAMOLE_HOME
+EOF
+
+# Install webapp
+ln -sf /opt/guacamole/webapp/guacamole.war $CATALINA_BASE/webapps/${WEBAPP_CONTEXT:-guacamole}.war
+
diff --git a/guacamole-docker/entrypoint.d/700-configure-features.sh b/guacamole-docker/entrypoint.d/700-configure-features.sh
new file mode 100644
index 0000000..f1a2b1a
--- /dev/null
+++ b/guacamole-docker/entrypoint.d/700-configure-features.sh
@@ -0,0 +1,88 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+#
+
+##
+## @fn 800-configure-features.sh
+##
+## Automatically checks all environment variables currently set and performs
+## configuration tasks related to those variabels, including installing any
+## extensions and external libraries associated with those variables to
+## GUACAMOLE_HOME.  Only environment variable prefixes are considered; this
+## script is not aware of whether an extension actually uses an environment
+## variable.
+##
+
+##
+## Returns whether the feature associated with a particular environment
+## variable prefix has configuration values set. Only the presence of
+## environment variables having that prefix is checked. Features can also be
+## entirely enabled/disabled through setting the [PREFIX_]ENABLED variable to
+## true/false respectively, where "[PREFIX_]" is the specified environment
+## variable prefix (including trailing underscore).
+##
+## @param VAR_BASE
+##     The environment variable prefix to check, including trailing underscore.
+##
+## @returns
+##     Zero if the feature associated with the given environment variable
+##     prefix is enabled, non-zero otherwise.
+##
+is_feature_enabled() {
+
+    local VAR_BASE="$1"
+
+    # Allow any feature to be explicitly enabled/disabled using a
+    # [PREFIX_]ENABLED variable
+    local ENABLED_VAR="${VAR_BASE}ENABLED"
+    if [ "${!ENABLED_VAR}" = "true" ]; then
+        return 0
+    elif [ "${!ENABLED_VAR}" = "false" ]; then
+        return 1
+    fi
+
+    # Lacking an explicit request to enable/disable the feature, rely on
+    # implicit enable/disable via presence of any other variables having the
+    # given prefix
+    awk 'BEGIN{for(v in ENVIRON) print v}' | grep "^${VAR_BASE}" > /dev/null
+
+}
+
+# Search environment for enabled extensions/features based on environment
+# variable prefixes
+for VAR_BASE in /opt/guacamole/environment/*; do
+
+    # Skip any directories without at least one corresponding environment
+    # variable set
+    is_feature_enabled "$(basename "$VAR_BASE")" || continue
+
+    # Execute any associated configuration script
+    [ ! -e "$VAR_BASE/configure.sh" ] || source "$VAR_BASE/configure.sh"
+
+    # Add any required links for extensions/libraries associated with the
+    # configured extension
+    for SUBDIR in lib extensions; do
+        if [ -d "$VAR_BASE/$SUBDIR" ]; then
+            mkdir -p "$GUACAMOLE_HOME/$SUBDIR/"
+            ln -s "$VAR_BASE/$SUBDIR"/* "$GUACAMOLE_HOME/$SUBDIR/"
+        fi
+    done
+
+done
+
diff --git a/guacamole-docker/entrypoint.d/999-start-tomcat.sh b/guacamole-docker/entrypoint.d/999-start-tomcat.sh
new file mode 100644
index 0000000..e31b4e3
--- /dev/null
+++ b/guacamole-docker/entrypoint.d/999-start-tomcat.sh
@@ -0,0 +1,30 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn 999-start-tomcat.sh
+##
+## Starts Tomcat. This script replaces the current process with the Tomcat
+## process and does not exit.
+##
+
+# Start tomcat
+cd /usr/local/tomcat
+exec catalina.sh run
+
diff --git a/guacamole-docker/environment/REMOTE_IP_VALVE_/configure.sh b/guacamole-docker/environment/REMOTE_IP_VALVE_/configure.sh
new file mode 100644
index 0000000..ad6e5ff
--- /dev/null
+++ b/guacamole-docker/environment/REMOTE_IP_VALVE_/configure.sh
@@ -0,0 +1,61 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+##
+## @fn REMOTE_IP_VALVE_/configure.sh
+##
+## Configures Tomcat to forward the IP addresses of clients behind a proxy if
+## the REMOTE_IP_VALVE_ENABLED environment variable is set to "true".
+##
+
+##
+## Array of all xmlstarlet command-line options necessary to add the
+## RemoteIpValve attributes that correspond to various "REMOTE_IP_VALVE_*"
+## environment variables.
+##
+declare -a VALVE_ATTRIBUTES=( --type attr -n className -v org.apache.catalina.valves.RemoteIpValve )
+
+# Translate all properties supported by RemoteIpValve into corresponding
+# environment variables
+for ATTRIBUTE in \
+    remoteIpHeader \
+    internalProxies \
+    proxiesHeader \
+    trustedProxies \
+    protocolHeader \
+    protocolHeaderHttpsValue \
+    httpServerPort \
+    httpsServerPort; do
+
+    VAR_NAME="REMOTE_IP_VALVE_$(echo "$ATTRIBUTE" | sed 's/\([a-z]\)\([A-Z]\)/\1_\2/g' | tr 'a-z' 'A-Z')"
+    if [ -n "${!VAR_NAME}" ]; then
+        VALVE_ATTRIBUTES+=( --type attr -n "$ATTRIBUTE" -v "${!VAR_NAME}" )
+    else
+        echo "Using default RemoteIpValve value for \"$ATTRIBUTE\" attribute."
+    fi
+
+done
+
+# Programmatically add requested RemoteIpValve entry
+xmlstarlet edit --inplace \
+    --insert '/Server/Service/Engine/Host/*' --type elem -n Valve \
+    --insert '/Server/Service/Engine/Host/Valve[not(@className)]' \
+    "${VALVE_ATTRIBUTES[@]}" \
+    "$CATALINA_BASE/conf/server.xml"
+
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/properties/EnumGuacamoleProperty.java b/guacamole-ext/src/main/java/org/apache/guacamole/properties/EnumGuacamoleProperty.java
index 255e4b0..a552993 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/properties/EnumGuacamoleProperty.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/properties/EnumGuacamoleProperty.java
@@ -20,6 +20,7 @@
 package org.apache.guacamole.properties;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
@@ -45,8 +46,10 @@
 
     /**
      * Defines the string value which should be accepted and parsed into the
-     * annotated enum constant.
+     * annotated enum constant. This annotation is repeatable, and each enum
+     * constant may be associated with any any number of string values.
      */
+    @Repeatable(PropertyValues.class)
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.FIELD)
     public static @interface PropertyValue {
@@ -64,6 +67,29 @@
     }
 
     /**
+     * Defines the string values which should be accepted and parsed into the
+     * annotated enum constant. Each enum constant may be associated with any
+     * any number of string values.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public static @interface PropertyValues {
+
+        /**
+         * Returns the {@link PropertyValue} annotations that represent the
+         * String values that should produce the annotated enum constant when
+         * parsed.
+         *
+         * @return
+         *     The {@link PropertyValue} annotations that represent the String
+         *     values that should produce the annotated enum constant when
+         *     parsed.
+         */
+        PropertyValue[] value();
+
+    }
+
+    /**
      * Mapping of valid property values to the corresponding enum constants
      * that those values parse to.
      */
@@ -103,7 +129,17 @@
                         + "match declared values.", e);
             }
 
-            // Map enum constant only if PropertyValue annotation is present
+            // Map enum constant only if one or more PropertyValue annotations
+            // are present
+            PropertyValues valuesAnnotation = field.getAnnotation(PropertyValues.class);
+            if (valuesAnnotation != null) {
+                for (PropertyValue valueAnnotation : valuesAnnotation.value())
+                    valueMapping.put(valueAnnotation.value(), value);
+            }
+
+            // The PropertyValue annotation may appear as a separate, single
+            // annotation, or as a multi-valued annotation via PropertyValues
+            // (see above)
             PropertyValue valueAnnotation = field.getAnnotation(PropertyValue.class);
             if (valueAnnotation != null)
                 valueMapping.put(valueAnnotation.value(), value);
diff --git a/guacamole-ext/src/test/java/org/apache/guacamole/properties/EnumGuacamolePropertyTest.java b/guacamole-ext/src/test/java/org/apache/guacamole/properties/EnumGuacamolePropertyTest.java
index 5b9a3d7..f2f313c 100644
--- a/guacamole-ext/src/test/java/org/apache/guacamole/properties/EnumGuacamolePropertyTest.java
+++ b/guacamole-ext/src/test/java/org/apache/guacamole/properties/EnumGuacamolePropertyTest.java
@@ -70,6 +70,7 @@
          * @see <a href="https://en.wikipedia.org/wiki/Tuna">Tuna (Wikipedia)</a>
          */
         @PropertyValue("tuna")
+        @PropertyValue("yellowfin")
         TUNA,
 
         /**
@@ -135,6 +136,7 @@
         assertEquals(Fish.TROUT,    FAVORITE_FISH.parseValue("trout"));
         assertEquals(Fish.MACKEREL, FAVORITE_FISH.parseValue("mackerel"));
         assertEquals(Fish.TUNA,     FAVORITE_FISH.parseValue("tuna"));
+        assertEquals(Fish.TUNA,     FAVORITE_FISH.parseValue("yellowfin"));
         assertEquals(Fish.SARDINE,  FAVORITE_FISH.parseValue("sardine"));
     }
 
@@ -164,7 +166,7 @@
         }
         catch (GuacamoleException e) {
             String message = e.getMessage();
-            assertTrue(message.contains("\"mackerel\", \"salmon\", \"sardine\", \"trout\", \"tuna\""));
+            assertTrue(message.contains("\"mackerel\", \"salmon\", \"sardine\", \"trout\", \"tuna\", \"yellowfin\""));
         }
     }
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java
index 1068477..d8e3f3d 100644
--- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java
+++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java
@@ -95,7 +95,7 @@
 
     /**
      * A property that determines whether environment variables are evaluated
-     * to override properties specified in guacamole.properties.
+     * to supply properties not specified in guacamole.properties.
      */
     private static final BooleanGuacamoleProperty ENABLE_ENVIRONMENT_PROPERTIES =
         new BooleanGuacamoleProperty() {
@@ -106,6 +106,19 @@
         };
 
     /**
+     * A property that determines whether environment variables of the form
+     * "*_FILE" are evaluated to supply properties not specified in
+     * guacamole.properties nor in environment variables.
+     */
+    private static final BooleanGuacamoleProperty ENABLE_FILE_ENVIRONMENT_PROPERTIES =
+        new BooleanGuacamoleProperty() {
+            @Override
+            public String getName() {
+                return "enable-file-environment-properties";
+            }
+        };
+
+    /**
      * The Guacamole server environment.
      */
     private Environment environment;
@@ -172,6 +185,23 @@
             logger.debug("Error reading \"{}\" property from guacamole.properties.", ENABLE_ENVIRONMENT_PROPERTIES.getName(), e);
         }
 
+        // For any values not defined in GUACAMOLE_HOME/guacamole.properties
+        // nor in the system environment, read from files pointed to by
+        // corresponding "*_FILE" variables in the system environment if
+        // "enable-file-environment-properties" is set to "true"
+        try {
+            if (environment.getProperty(ENABLE_FILE_ENVIRONMENT_PROPERTIES, false)) {
+                environment.addGuacamoleProperties(new SystemFileEnvironmentGuacamoleProperties());
+                logger.info("Additional configuration parameters may be read "
+                        + "from files pointed to by \"*_FILE\" environment "
+                        + "variables.");
+            }
+        }
+        catch (GuacamoleException e) {
+            logger.error("Unable to configure support for file environment properties: {}", e.getMessage());
+            logger.debug("Error reading \"{}\" property from guacamole.properties.", ENABLE_FILE_ENVIRONMENT_PROPERTIES.getName(), e);
+        }
+
         // Now that at least the main guacamole.properties source of
         // configuration information is available, initialize the session map
         sessionMap = new HashTokenSessionMap(environment);
@@ -210,13 +240,24 @@
                 return current;
 
             // Create new injector if necessary
-            Injector injector = Guice.createInjector(Stage.PRODUCTION,
-                new EnvironmentModule(environment),
-                new LogModule(environment),
-                new ExtensionModule(environment),
-                new RESTServiceModule(sessionMap),
-                new TunnelModule()
-            );
+            Injector injector =
+
+                    // Ensure environment and logging are configured FIRST ...
+                    Guice.createInjector(Stage.PRODUCTION,
+                        new EnvironmentModule(environment),
+                        new LogModule(environment)
+                    )
+
+                    // ... before attempting configuration of any other modules
+                    // (logging within the constructors of other modules may
+                    // otherwise default to including messages from the "debug"
+                    // level, regardless of how the application log level is
+                    // actually configured)
+                    .createChildInjector(
+                        new ExtensionModule(environment),
+                        new RESTServiceModule(sessionMap),
+                        new TunnelModule()
+                    );
 
             return injector;
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/SystemFileEnvironmentGuacamoleProperties.java b/guacamole/src/main/java/org/apache/guacamole/SystemFileEnvironmentGuacamoleProperties.java
new file mode 100644
index 0000000..8b08252
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/SystemFileEnvironmentGuacamoleProperties.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole;
+
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.guacamole.properties.GuacamoleProperties;
+import org.apache.guacamole.token.TokenName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GuacamoleProperties implementation which reads all properties from files
+ * whose filenames are stored in environment variables. The name of the
+ * environment variable corresponding to the filename is determined from the
+ * original property using {@link TokenName#canonicalize(java.lang.String)}
+ * with an additional "_FILE" suffix.
+ */
+public class SystemFileEnvironmentGuacamoleProperties implements GuacamoleProperties {
+
+    /**
+     * Logger for this class.
+     */
+    private static final Logger logger = LoggerFactory.getLogger(SystemFileEnvironmentGuacamoleProperties.class);
+
+    @Override
+    public String getProperty(String name) {
+
+        String filename = System.getenv(TokenName.canonicalize(name) + "_FILE");
+        if (filename != null) {
+            try {
+                return Files.asCharSource(new File(filename), StandardCharsets.UTF_8).read();
+            }
+            catch (IOException e) {
+                logger.error("Property \"{}\" could not be read from file \"{}\": {}", name, filename, e.getMessage());
+                logger.debug("Error reading property value from file.", e);
+            }
+        }
+
+        return null;
+
+    }
+
+}
+
diff --git a/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java b/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java
new file mode 100644
index 0000000..27bf0fe
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.log;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue;
+
+/**
+ * All log levels supported by the Apache Guacamole web application. Each log
+ * level describes a different level of verbosity for the log messages included
+ * in web application logs.
+ */
+public enum LogLevel {
+
+    /**
+     * Errors that are fatal in the context of the operation being logged.
+     */
+    @PropertyValue("error")
+    ERROR("error"),
+
+    /**
+     * Non-fatal conditions that may indicate the presence of a problem.
+     */
+    @PropertyValue("warning")
+    @PropertyValue("warn")
+    WARNING("warning"),
+
+    /**
+     * Informational messages of general interest to users or administrators.
+     */
+    @PropertyValue("info")
+    INFO("info"),
+
+    /**
+     * Informational messages that are useful for debugging, but are generally
+     * not useful to users or administrators. It is expected that debug-level
+     * messages, while verbose, will not affect performance.
+     */
+    @PropertyValue("debug")
+    DEBUG("debug"),
+
+    /**
+     * Informational messages that may be useful for debugging, but which are
+     * so low-level that they may affect performance.
+     */
+    @PropertyValue("trace")
+    TRACE("trace");
+
+    /**
+     * Format string whose sole format argument is a String containing the
+     * name of the log level. As this configuration will be fed to Logback, the
+     * name used must be a name acceptable by Logback.
+     */
+    private static final String LOGBACK_XML_TEMPLATE =
+              "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<configuration>\n"
+            + "\n"
+            + "    <!-- Default appender -->\n"
+            + "    <appender name=\"GUAC-DEFAULT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n"
+            + "        <encoder>\n"
+            + "            <pattern>%%d{HH:mm:ss.SSS} [%%thread] %%-5level %%logger{36} - %%msg%%n</pattern>\n"
+            + "        </encoder>\n"
+            + "    </appender>\n"
+            + "\n"
+            + "    <!-- Log at level defined with \"log-level\" property -->\n"
+            + "    <root level=\"%s\">\n"
+            + "        <appender-ref ref=\"GUAC-DEFAULT\" />\n"
+            + "    </root>\n"
+            + "\n"
+            + "</configuration>\n";
+
+    /**
+     * The name that should be used to refer to this log level in the context
+     * of configuring Guacamole. This name should be both descriptive and
+     * acceptable as the value of the "log-level" property.
+     */
+    private final String canonicalName;
+
+    /**
+     * The raw contents of the "logback.xml" that configures Logback to log
+     * messages at this level, encoded as UTF-8.
+     */
+    private final byte[] logbackConfig;
+
+    /**
+     * Creates a new LogLevel with the given names. The pair of names provided
+     * correspond to the name used within Guacamole's configuration and the
+     * name used within Logback's configuration.
+     *
+     * @param canonicalName
+     *     The name that should be used for this log level when configuring
+     *     Guacamole to log at this level using the "log-level" property.
+     *
+     * @param logbackLogLevel
+     *     The name that would be provided to Logback to log at this level if
+     *     manually configuring Logback using "logback.xml".
+     */
+    private LogLevel(String canonicalName, String logbackLogLevel) {
+        this.canonicalName = canonicalName;
+        this.logbackConfig = String.format(LOGBACK_XML_TEMPLATE, logbackLogLevel).getBytes(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Creates a new LogLevel with the given name. The provided name corresponds
+     * to both the name used within Guacamole's configuration and the name used
+     * within Logback's configuration.
+     *
+     * @param logLevel
+     *     The name that should be used for this log level when configuring
+     *     Guacamole to log at this level using the "log-level" property AND
+     *     when manually configuring Logback to log at this level using a
+     *     "logback.xml" configuration file.
+     */
+    private LogLevel(String logLevel) {
+        this(logLevel, logLevel);
+    }
+
+    /**
+     * Returns a name that may be used to refer to this log level when
+     * configuring Guacamole using the "log-level" property.
+     *
+     * @return
+     *     A name that may be used to refer to this log level when
+     *     configuring Guacamole using the "log-level" property.
+     */
+    public String getCanonicalName() {
+        return canonicalName;
+    }
+
+    /**
+     * Returns a new InputStream that streams the contents of an XML
+     * configuration file that can be provided to Logback to configure logging
+     * at this log level.
+     *
+     * @return
+     *     A a new InputStream that streams the contents of an XML
+     *     configuration file that can be provided to Logback to configure
+     *     logging at this log level.
+     */
+    public InputStream getLogbackConfiguration() {
+        return new ByteArrayInputStream(logbackConfig);
+    }
+
+}
diff --git a/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java b/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java
index accc4a9..a9e4ac3 100644
--- a/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java
+++ b/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java
@@ -25,7 +25,13 @@
 import ch.qos.logback.core.util.StatusPrinter;
 import com.google.inject.AbstractModule;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.properties.EnumGuacamoleProperty;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,6 +51,19 @@
     private final Environment environment;
 
     /**
+     * Property that specifies the highest level of verbosity that Guacamole
+     * should use for the messages in its logs.
+     */
+    private static final EnumGuacamoleProperty<LogLevel> LOG_LEVEL = new EnumGuacamoleProperty<LogLevel>(LogLevel.class) {
+
+        @Override
+        public String getName() {
+            return "log-level";
+        }
+
+    };
+
+    /**
      * Creates a new LogModule which uses the given environment to determine
      * the logging configuration.
      *
@@ -54,26 +73,57 @@
     public LogModule(Environment environment) {
         this.environment = environment;
     }
-    
+
+    /**
+     * Returns an InputStream that streams the contents of the "logback.xml"
+     * file that Logback should read to configure logging to Guacamole. If the
+     * user provided their own "logback.xml" within GUACAMOLE_HOME, this will
+     * be an InputStream that reads the contents of that file. The required
+     * "logback.xml" will otherwise be dynamically generated based on the value
+     * of the "log-level" property.
+     *
+     * @return
+     *     An InputStream that streams the contents of the "logback.xml" file
+     *     that Logback should read to configure logging to Guacamole.
+     */
+    private InputStream getLogbackConfiguration() {
+
+        // Check for custom logback.xml
+        File logbackFile = new File(environment.getGuacamoleHome(), "logback.xml");
+        if (logbackFile.exists()) {
+            try {
+                logger.info("Loading logback configuration from \"{}\".", logbackFile);
+                return new FileInputStream(logbackFile);
+            }
+            catch (FileNotFoundException e) {
+                logger.info("Logback configuration could not be read "
+                        + "from \"{}\": {}", logbackFile, e.getMessage());
+            }
+        }
+
+        // Default to generating an internal logback.xml based on a simple
+        // "log-level" property
+        LogLevel level;
+        try {
+            level = environment.getProperty(LOG_LEVEL, LogLevel.INFO);
+            logger.info("Logging will be at the \"{}\" level.", level.getCanonicalName());
+        }
+        catch (GuacamoleException e) {
+            level = LogLevel.INFO;
+            logger.error("Falling back to \"{}\" log level: {}", level.getCanonicalName(), e.getMessage());
+        }
+
+        return level.getLogbackConfiguration();
+
+    }
+
     @Override
     protected void configure() {
 
-        // Only load logback configuration if GUACAMOLE_HOME exists
-        File guacamoleHome = environment.getGuacamoleHome();
-        if (!guacamoleHome.isDirectory())
-            return;
+        try (InputStream logbackConfiguration = getLogbackConfiguration()) {
 
-        // Check for custom logback.xml
-        File logbackConfiguration = new File(guacamoleHome, "logback.xml");
-        if (!logbackConfiguration.exists())
-            return;
-
-        logger.info("Loading logback configuration from \"{}\".", logbackConfiguration);
-
-        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
-        context.reset();
-
-        try {
+            LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+            context.reset();
 
             // Initialize logback
             JoranConfigurator configurator = new JoranConfigurator();
@@ -86,7 +136,11 @@
         }
         catch (JoranException e) {
             logger.error("Initialization of logback failed: {}", e.getMessage());
-            logger.debug("Unable to load logback configuration..", e);
+            logger.debug("Unable to load logback configuration.", e);
+        }
+        catch (IOException e) {
+            logger.warn("Logback configuration file could not be cleanly closed: {}", e.getMessage());
+            logger.debug("Failed to close logback configuration file.", e);
         }
 
     }
diff --git a/guacamole/src/main/resources/logback.xml b/guacamole/src/main/resources/logback.xml
deleted file mode 100644
index 0b91a42..0000000
--- a/guacamole/src/main/resources/logback.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
--->
-<configuration>
-
-    <!-- Default appender -->
-    <appender name="GUAC-DEFAULT" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder>
-            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
-        </encoder>
-    </appender>
-
-    <!-- Log at INFO level -->
-    <root level="info">
-        <appender-ref ref="GUAC-DEFAULT" />
-    </root>
-
-</configuration>
\ No newline at end of file