This post will guide you through on how one of our employees set up an entire ForgeRock AM development environment connecting to replicated external Directory Services (DS) and a resource to protect with Apache web agent on different containers with the help of Docker and Docker Compose. This can help simulate a production environment where there is separation of services on different servers.

Setup

  1. Ensure Docker and Docker Compose is installed on your machine, instructions to install it can be located on the Docker website at https://docs.docker.com/compose/install/
  2. Pull the source codes from our GitHub at: https://github.com/Nebulas-Tree/ForgeRock-Docker-Compose
  3. Download the official Access Manager (AM) WAR file from ForgeRock at https://backstage.forgerock.com/downloads/browse/am/latest and place it at [Path to Source Codes] -> buildfiles -> am -> AM.war
  4. Download the official Amster ZIP file from ForgeRock from the same link as step 2 and place it at [Path to Source Codes] -> buildfiles -> am -> Amster.zip
  5. Download the official Linux Apache Web Agent (WA) from https://backstage.forgerock.com/downloads/browse/am/latest/web-agents and place it at [Path to Source Codes] -> buildfiles -> httpd -> web-agent.zip
  6. Download the official Directory Services (DS) ZIP file ForgeRock at https://backstage.forgerock.com/downloads/browse/ds/latest and place it at [Path to Source Codes] -> buildfiles -> ds -> DS.zip
  7. Run docker-compose up --build to start building the environment and eventually it will start.
  8. Set your computer's local hosts file to point am.example.com, cts.example.com, cts2.example.com, cfg1.example.com, cfg2.example.com to your localhost 127.0.0.1.
  9. Run this command if you wish to use Amster to bootstrap the initial configuration: docker exec am.example.com ./bootstrap.sh (desired AMAdmin password) cfg.example.com. Alternatively, you can run Amster interactively using: docker exec -it am.example.com ./amster or use the web configurator wizard at http://am.example.com:8080/am to set it up, do follow the image file configurator_summary_details.png for the values for configuration.
  10. Finally, you can access AM at http://am.example.com:8080/am and tinker away.

Docker-Compose guide

.env file

  • DS_BASEDN: the base DN of all DS profiles

Directory Service

  • container_name and hostname to be the same Fully Qualified Domain Name (FQDN)
  • build must contain context: buildfiles/ds
  • image is can be anything you want to call the DS image
  • ports require external ports for the internal ports 80, 443, 389, 646, 4444 to be configured
  • volumes will require persistent volume for the  internal path /root/.opendj/ to be configured
  • networks give any ipv4_address that is available, preferably in the same subnet.
  • env_file leave it as .env
  • environment:
    HOST must be the same as container_name
    TYPE is a list of types that can be delimited by any character, it can contain the few keywords:
    Starting with directory or replication: directory or replication-only server
    Contains replication: to configure replication
    Requires MASTER_HOST and MASTER_ROOT_PASS environment variables set to the FQDN and root password of the DS you wish to replicate this instance with.
    Requires SLAVE_HOST environment variable to be the same as HOST
    Contains cts or tokens: setup using Core Token Service (CTS) profile
    Requires DS_CTS_PASS environment variable to set the CTS data store admin password
    Contains cfg or config: setup using AM configuration profile
    Requires DS_CONFIG_PASS environment variable to set the config data store admin password
    Contains ids or user: setup using identity store profile
    Requires DS_IDS_PASS environment variable to set the config data
    DS_ROOT_PASS is the root user cn=Directory Manager's password
    DS_MONITOR_PASS is the monitor user's password
    i.e. directory-replication-cts-cfg-ids

Access Manager

  • container_name and hostname to be the same Fully Qualified Domain Name (FQDN)
  • build must contain context: buildfiles/am
  • image is can be anything you want to call the AM image
  • ports require external ports for the internal ports 8080 to be configured
  • volumes will require persistent volume for the  internal path /root/am and/root/.openamcfg to be configured. /tmp/treenodes is optional for additional tree nodes.
  • networks give any ipv4_address that is available, preferably in the same subnet.
  • env_file leave it as .env
  • environment:
    DIRECTORIES a list of FQDN and port of DS LDAPS ports delimited by a space to automatically query and trust certificates with.

Explanation of Shell Scripts

Access Manager

docker.sh

The shell script the Docker container will initialize with.

# Setup fake group
getent group fakegroup >/dev/null || addgroup --gid ${GID:-1000} fakegroup && chgrp -R fakegroup /root && chmod -R g=rXs /root
Creates a underprivileged group so the volumes will not be owned by root.
# Copy all custom treenodes into AM
cp -urf /tmp/treenodes/* ${CATALINA_HOME}/webapps/am/WEB-INF/lib/
Copies all custom tree nodes from [Path to Source Codes] -> volumes -> am -> treenodes to AM itself.
# Trust all directory certificates specified in DIRECTORIES with space as delimiter
IFS=' '
read -ra ARR_DIRECTORIES <<< "${DIRECTORIES}"
for DIRECTORY in "${ARR_DIRECTORIES[@]}"; do
    until openssl s_client -connect "${DIRECTORY}" -showcerts >/dev/null 2>/dev/null; do
      >&2 echo "Waiting for ${DIRECTORY} to start..."
      sleep 5
    done
    echo "" | openssl s_client -connect "${DIRECTORY}" -showcerts 2>/dev/null | openssl x509 -out /tmp/cert
    keytool -importcert -alias "${DIRECTORY}" -file /tmp/cert -trustcacerts -keystore ${JAVA_HOME}/jre/lib/security/cacerts -storetype JKS -storepass changeit -noprompt
    echo -e "\t- ${DIRECTORY}"
done
Automatically trust certificates from specified list of DS FQDNs.
# Show bootstrap.sh guide if not set up
if [[ ! -f /root/am/install.log ]]; then
    echo `
        until [[ "$(cat ${CATALINA_HOME}/logs/catalina.out | grep 'org.apache.catalina.startup.Catalina.start')" != "" ]];
        do
          sleep 5
        done
        echo -e "\n\nRun 'docker exec ${HOSTNAME} ./bootstrap.sh (PASSWORD) (DS/CFG HOSTNAME) (IDS HOSTNAME [OPTIONAL])' to quickly bootstrap using Amster."
    ` & 2>/dev/null
fi
A guiding line after startup, if AM is not set up yet.
# Start Tomcat and AM
startup.sh
tail -f ${CATALINA_HOME}/logs/catalina.out
Start Tomcat and follow the logs for the Docker output.

bootstrap.sh

Used to quickly configure a basic AM instance using Amster.

# Check number of variables
if [[ $# -lt 2 ]]; then
	echo "Usage: $0 (PASSWORD) (DS/CFG HOSTNAME) (IDS HOSTNAME [OPTIONAL])"
	exit 1
fi
Enforce number of arguments
# Execute Amster install-openam
./amster <<< "install-openam \
  --serverUrl http://${HOSTNAME}:8080/am \
  --adminPwd $1 \
  --acceptLicense \
  --cfgDir /root/am \
  --cfgStore dirServer \
  --cfgStoreHost $2 \
  --cfgStorePort 636 \
  --cfgStoreDirMgr uid=am-config,ou=admins,ou=am-config,${DS_BASEDN} \
  --cfgStoreRootSuffix ou=am-config,${DS_BASEDN} \
  --cfgStoreSsl SSL \
  --userStoreType LDAPv3ForOpenDS \
  --userStoreHost ${3:-$2} \
  --userStorePort 636 \
  --userStoreDirMgr uid=am-identity-bind-account,ou=admins,ou=identities,${DS_BASEDN} \
  --userStoreRootSuffix ou=identities,${DS_BASEDN} \
  --userStoreDirMgrPwd $1 \
  --userStoreSsl SSL
:exit"
Execute Amster with arguments

setenv.sh

CATALINA_OPTS="-server -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC"
CATALINA_PID="$CATALINA_BASE/bin/catalina.pid"
Set recommended Java Virtual Machine (JVM) arguments for Tomcat

Directory Service

docker.sh

if [[ ! -f /root/.opendj/instance.loc ]]; then
    rm -f instance.loc
Only run this block of code if DS is not set up yet.
    if [[ "${TYPE}" == "directory"* ]]; then
Check if TYPE environment variable starts with directory.
        # Directory server
        params+=(
            directory-server \
            --instancePath "/root/.opendj" \
            --monitorUserDn "uid=Monitor" \
            --monitorUserPassword ${DS_MONITOR_PASS} \
            --ldapPort 389 \
            --enableStartTls \
            --ldapsPort 636 \
            --httpPort 80 \
            --httpsPort 443 \
        )
Basic parameters for a directory server using DS_MONITOR.
        # Config store
        [[ "${TYPE}" == *"cfg"* || "${TYPE}" == *"config"* ]] && params+=(
            --profile am-config \
            --set am-config/amConfigAdminPassword:${DS_CONFIG_PASS} \
            --set am-config/baseDn:ou=am-config,${DS_BASEDN} \
        )
        # CTS
        [[ "${TYPE}" == *"cts"* || "${TYPE}" == *"tokens"* ]] && params+=(
            --profile am-cts \
            --set am-cts/amCtsAdminPassword:${DS_CTS_PASS} \
            --set am-cts/baseDn:ou=tokens,${DS_BASEDN} \
        )
        # User store
        [[ "${TYPE}" == *"ids"* || "${TYPE}" == *"user"* ]] && params+=(
            --profile am-identity-store \
            --set am-identity-store/amIdentityStoreAdminPassword:${DS_IDS_PASS} \
            --set am-identity-store/baseDn:ou=identities,${DS_BASEDN} \
        )
If TYPE environment variable contains cfg, config, cts, tokens, ids, user, use profiles feature to quickly set up the respective keyword store.
    elif [[ "${TYPE}" == "replication"* ]]; then
If TYPE environment variable starts with replication
        # Replication only
        params+=(
            replication-server \
            --replicationPort 8989 \
        )
Replication-only server parameters.
    # Common parameters
    params+=(
        --rootUserDn "cn=Directory Manager" \
        --rootUserPassword ${DS_ROOT_PASS} \
        --hostname ${HOSTNAME} \
        --adminConnectorPort 4444 \
        --productionMode \
        --acceptLicense \
        --doNotStart
    )
    ./setup "${params[@]}"
    echo "./setup ${params[@]}"
Append common parameters to the list and run DS setup with the parameters.
    # Tweaks and hardening
    bin/dsconfig set-password-policy-prop \
        --offline \
        --policy-name "Default Password Policy" \
        --set skip-validation-for-administrators:true \
        --no-prompt
Skip validating password policy for administrators for easier development.
    bin/dsconfig set-connection-handler-prop \
        --offline \
        --handler-name LDAPS \
        --set ssl-protocol:TLSv1.2 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA \
        --no-prompt
    bin/dsconfig set-administration-connector-prop \
        --offline \
        --set ssl-protocol:TLSv1.2 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA \
        --no-prompt
    bin/dsconfig set-crypto-manager-prop \
        --offline \
        --set ssl-protocol:TLSv1.2 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA \
        --set ssl-cipher-suite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA \
        --no-prompt
Limit to TLSv1.2 and secure cipher cuites for SSL and LDAPS connections.
    cp instance.loc /root/.opendj/instance.loc
Copy the instance.loc file to a persistent location for the check above.
# Start DS
bin/start-ds --noDetach &
Start DS.
# Setup replication
if [[ "${TYPE}" == *"replication"* ]]; then
If replication exists anywhere in the TYPE environment variable
    until bin/status -h ${MASTER_HOST} -p 4444 --bindPassword ${MASTER_ROOT_PASS} -s --trustAll --no-prompt > /dev/null; do
      >&2 echo "Waiting for master to start..."
      sleep 5
    done
Wait until DS server (MASTER_HOST environment variable) to be replicated from to be up
    params=(
        configure \
        --host1 ${MASTER_HOST} \
        --port1 4444 \
        --bindDn1 "cn=Directory Manager" \
        --bindPassword1 ${MASTER_ROOT_PASS} \
        --host2 ${SLAVE_HOST:-$HOSTNAME} \
        --port2 4444 \
        --bindDn2 "cn=Directory Manager" \
        --bindPassword2 ${DS_ROOT_PASS} \
        --replicationPort1 8989 \
        --secureReplication1 \
        --replicationPort2 8989 \
        --secureReplication2 \
        --baseDn ou=am-config,${DS_BASEDN} \
        --baseDn ou=identities,${DS_BASEDN} \
        --baseDn ou=tokens,${DS_BASEDN} \
        --baseDn uid=Monitor \
        --adminUid "cn=Directory Manager" \
        --adminPassword ${MASTER_ROOT_PASS} \
        --no-prompt --trustAll
    )
    bin/dsreplication "${params[@]}"
    echo "bin/dsreplication ${params[@]}"
Gather required parameters from the environment variables and configure replication.
    params=(
        initialize \
        --hostSource ${MASTER_HOST} \
        --portSource 4444 \
        --hostDestination ${SLAVE_HOST:-$HOST} \
        --portDestination 4444 \
        --baseDn ou=am-config,${DS_BASEDN} \
        --baseDn ou=identities,${DS_BASEDN} \
        --baseDn ou=tokens,${DS_BASEDN} \
        --baseDn uid=Monitor \
        --adminUid "cn=Directory Manager" \
        --adminPassword ${MASTER_ROOT_PASS} \
        --no-prompt --trustAll
    )
    bin/dsreplication "${params[@]}"
    echo "bin/dsreplication ${params[@]}"
Gather required parameters from the environment variables and initialize initial replication.
# Setup fake group
getent group fakegroup >/dev/null || addgroup --gid ${GID:-1000} fakegroup && chgrp -R fakegroup /root && chmod -R g=rXs /root
Creates a underprivileged group so the volumes will not be owned by root.
# Keep Docker container alive
tail -f /dev/null
Keep container alive by tailing /dev/null

Disclaimer

The document and content made available by Nebulas Tree in no way conveys any right, title, interest or license in any intellectual property rights (including but not limited to patents, copyrights, trade secrets or trademarks) contained herein. Nebulas Tree reserves the right to vary the terms of the document and content in response to changes to the specifications or information made available to Nebulas Tree.

Nebulas Tree does not assume liability for any errors or omissions in the content of this document or any referenced or associated third party document, including, but not limited to, typographical errors, inaccuracies or outdated information.  This document and all information within it are provided on an "as is" basis without any warranties of any kind, express or implied.  Any communication required or permitted in terms of this document shall be valid and effective only if submitted in writing. Reliance of any information provided herein this document is solely at your own risk.