diff --git a/.gitignore b/.gitignore index 4e42e1352..e002236c1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ src/ui/package-lock.json # Docker Compose persistent storage directory src/packaging/data + +# SSL stores directory +src/packaging/ssl-stores diff --git a/src/docs/content/docs.md b/src/docs/content/docs.md index ed855b3b3..cb07c6471 100644 --- a/src/docs/content/docs.md +++ b/src/docs/content/docs.md @@ -384,7 +384,11 @@ Note that the above command will build the Reaper JAR and place it in the _src/s ### Start Docker Environment -From the top level directory change to the _src/packaging_ directory +The `docker-compose` services available allow for orchestration of an environment that uses default settings. In addition, services are provided that allow orchestration of an environment in which the connections between the services are SSL encrypted. Services which use SSL encryption contain a `-ssl` suffix in their name. + +#### Default Settings Environment + +From the top level directory change to the _src/packaging_ directory: ```bash cd src/packaging @@ -407,7 +411,7 @@ Once the Cassandra node is online and accepting CQL connections, create the requ By default, the `reaper_db` keyspace is created using a replication factor of 1. To change this replication factor, provide the intended replication factor as an optional argument: ```bash -docker-compose run initialize-reaper_db [$REPLICATION_FACTOR] +docker-compose run cqlsh-initialize-reaper_db [$REPLICATION_FACTOR] ``` Wait a few moments for the `reaper_db` schema change to propagate, then start Reaper: @@ -416,6 +420,46 @@ Wait a few moments for the `reaper_db` schema change to propagate, then start Re docker-compose up reaper ``` +#### SSL Encrypted Connections Environment + +From the top level directory change to the _src/packaging_ directory: + +```bash +cd src/packaging +``` + +Generate the SSL Keystore and Truststore which will be used to encrypt the connections between Reaper and Cassandra. + +```bash +docker-compose run generate-ssl-stores +``` + +Start the Cassandra cluster which encrypts both the JMX and Native Protocol: + +```bash +docker-compose up cassandra-ssl +``` + +The `nodetool-ssl` Docker Compose service can be used to check on the Cassandra node's status: + +```bash +docker-compose run nodetool-ssl status +``` + +Once the Cassandra node is online and accepting encrypted SSL connections via the Native Transport protocol, create the required `reaper_db` Cassandra keyspace to allow Reaper to save its cluster and scheduling data. + +By default, the `reaper_db` keyspace is created using a replication factor of 1. To change this replication factor, provide the intended replication factor as an optional argument: + +```bash +docker-compose run cqlsh-initialize-reaper_db-ssl [$REPLICATION_FACTOR] +``` + +Wait a few moments for the `reaper_db` schema change to propagate, then start the Reaper service that will establish encrypted connections to Cassandra: + +```bash +docker-compose up reaper-ssl +``` + ### Access The Environment @@ -429,12 +473,20 @@ When adding the Cassandra node to the Reaper UI, use the IP address found via: docker-compose run nodetool status ``` -The helper `cqlsh` Docker Compose service has also been included: +The helper `cqlsh` Docker Compose service has also been included for both the default and SSL encrypted environments: + +#### Default Environment ```bash docker-compose run cqlsh ``` +#### SSL Encrypted Environment + +```bash +docker-compose run cqlsh-ssl +``` + ### Destroying the Docker Environment When terminating the infrastructure, use the following command to stop diff --git a/src/packaging/common-services.yml b/src/packaging/common-services.yml new file mode 100644 index 000000000..10d3dfa8a --- /dev/null +++ b/src/packaging/common-services.yml @@ -0,0 +1,37 @@ +version: '2.1' + +services: + cassandra-common: + image: cassandra:3.11 + env_file: + - ./docker-services/cassandra/cassandra.env + mem_limit: 4g + memswap_limit: 4g + mem_swappiness: 0 + ports: + - "7000:7000" + - "7001:7001" + - "7199:7199" + - "9042:9042" + volumes: + - ./data/cassandra:/var/lib/cassandra + - ./docker-services/cassandra/jmxremote.access:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/management/jmxremote.access + - ./docker-services/cassandra/jmxremote.password:/etc/cassandra/jmxremote.password + + cqlsh-common: + build: ./docker-services/cqlsh + env_file: + - ./docker-services/cqlsh/cql.env + + nodetool-common: + build: ./docker-services/nodetool + env_file: + - ./docker-services/nodetool/nodetool.env + + reaper-common: + image: cassandra-reaper:latest + env_file: + - ./docker-services/reaper/reaper.env + ports: + - "8080:8080" + - "8081:8081" \ No newline at end of file diff --git a/src/packaging/docker-compose.yml b/src/packaging/docker-compose.yml index 7b097bce0..b8c44018f 100644 --- a/src/packaging/docker-compose.yml +++ b/src/packaging/docker-compose.yml @@ -2,48 +2,110 @@ version: '2.1' services: cassandra: - image: cassandra:3.11 + extends: + file: ./common-services.yml + service: cassandra-common + + cassandra-ssl: + extends: + file: ./common-services.yml + service: cassandra-common + entrypoint: /docker-alt-entrypoint.sh cassandra -f env_file: - - ./docker-services/cassandra/cassandra.env - mem_limit: 4g - memswap_limit: 4g - mem_swappiness: 0 - ports: - - "7000:7000" - - "7001:7001" - - "7199:7199" - - "9042:9042" + - ./docker-services/cassandra-ssl/cassandra-ssl.env volumes: - - ./data/cassandra:/var/lib/cassandra - - ./docker-services/cassandra/jmxremote.access:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/management/jmxremote.access - - ./docker-services/cassandra/jmxremote.password:/etc/cassandra/jmxremote.password + - ./ssl-stores/cassandra-server-keystore.jks:/etc/ssl/cassandra-server-keystore.jks + - ./ssl-stores/generic-server-truststore.jks:/etc/ssl/generic-server-truststore.jks + - ./docker-services/cassandra-ssl/docker-entrypoint.sh:/docker-alt-entrypoint.sh cqlsh: - image: cassandra:3.11 - command: cqlsh cassandra + extends: + file: ./common-services.yml + service: cqlsh-common links: - cassandra - initialize-reaper_db: - build: ./docker-services/initialize-reaper_db + cqlsh-ssl: + extends: + file: ./common-services.yml + service: cqlsh-common + env_file: + - ./docker-services/cqlsh-ssl/cqlsh-ssl.env + links: + - cassandra-ssl + volumes: + - ./ssl-stores/cassandra-server-keystore.jks:/etc/ssl/cassandra-server-keystore.jks + - ./ssl-stores/ca-cert:/etc/ssl/ca-cert + + cqlsh-initialize-reaper_db: + extends: + file: ./common-services.yml + service: cqlsh-common + entrypoint: /docker-alt-entrypoint.sh + env_file: + - ./docker-services/cqlsh-initialize-reaper_db/cqlsh-initialize-reaper_db.env links: - cassandra + volumes: + - ./docker-services/cqlsh-initialize-reaper_db/docker-entrypoint.sh:/docker-alt-entrypoint.sh + + cqlsh-initialize-reaper_db-ssl: + extends: + file: ./common-services.yml + service: cqlsh-common + entrypoint: /docker-alt-entrypoint.sh + env_file: + - ./docker-services/cqlsh-initialize-reaper_db-ssl/cqlsh-initialize-reaper_db-ssl.env + links: + - cassandra-ssl + volumes: + - ./docker-services/cqlsh-initialize-reaper_db-ssl/docker-entrypoint.sh:/docker-alt-entrypoint.sh + - ./ssl-stores/cassandra-server-keystore.jks:/etc/ssl/cassandra-server-keystore.jks + - ./ssl-stores/ca-cert:/etc/ssl/ca-cert + + generate-ssl-stores: + build: ./docker-services/generate-ssl-stores + volumes: + - ./resource/ca_cert.conf:/usr/src/app/ca_cert.conf + - ./ssl-stores:/usr/src/app/ssl-stores nodetool: - image: cassandra:3.11 - entrypoint: nodetool --host cassandra --username reaperUser --password reaperPass + extends: + file: ./common-services.yml + service: nodetool-common links: - cassandra - reaper: - image: cassandra-reaper:latest + nodetool-ssl: + extends: + file: ./common-services.yml + service: nodetool-common env_file: - - ./docker-services/reaper/reaper.env + - ./docker-services/nodetool-ssl/nodetool-ssl.env + links: + - cassandra-ssl + volumes: + - ./ssl-stores/cassandra-server-keystore.jks:/etc/ssl/cassandra-server-keystore.jks + - ./ssl-stores/generic-server-truststore.jks:/etc/ssl/generic-server-truststore.jks + + reaper: + extends: + file: ./common-services.yml + service: reaper-common links: - cassandra - ports: - - "8080:8080" - - "8081:8081" + + reaper-ssl: + extends: + file: ./common-services.yml + service: reaper-common + env_file: + - ./docker-services/reaper-ssl/reaper-ssl.env + links: + - cassandra-ssl + volumes: + - ./ssl-stores/reaper-server-keystore.jks:/etc/ssl/reaper-server-keystore.jks + - ./ssl-stores/generic-server-truststore.jks:/etc/ssl/generic-server-truststore.jks reaper-build-packages: extends: diff --git a/src/packaging/docker-services/cassandra-ssl/cassandra-ssl.env b/src/packaging/docker-services/cassandra-ssl/cassandra-ssl.env new file mode 100644 index 000000000..b66dbacd7 --- /dev/null +++ b/src/packaging/docker-services/cassandra-ssl/cassandra-ssl.env @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +CASSANDRA_KEYSTORE_PASSWORD=keypassword +CASSANDRA_TRUSTSTORE_PASSWORD=trustpassword \ No newline at end of file diff --git a/src/packaging/docker-services/cassandra-ssl/docker-entrypoint.sh b/src/packaging/docker-services/cassandra-ssl/docker-entrypoint.sh new file mode 100755 index 000000000..1b38dc5ba --- /dev/null +++ b/src/packaging/docker-services/cassandra-ssl/docker-entrypoint.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# copied from: +# https://github.com/docker-library/cassandra/blob/d83b850cd17bc9198876f8686197c730e29c7448/2.1/docker-entrypoint.sh + +set -e + +# first arg is `-f` or `--some-option` +if [ "${1:0:1}" = '-' ]; then + set -- cassandra -f "$@" +fi + +# allow the container to be started with `--user` +if [ "$1" = 'cassandra' -a "$(id -u)" = '0' ]; then + chown -R cassandra /var/lib/cassandra /var/log/cassandra "$CASSANDRA_CONFIG" + exec gosu cassandra "$BASH_SOURCE" "$@" +fi + +if [ "$1" = 'cassandra' ]; then + : ${CASSANDRA_RPC_ADDRESS='0.0.0.0'} + + : ${CASSANDRA_LISTEN_ADDRESS='auto'} + if [ "$CASSANDRA_LISTEN_ADDRESS" = 'auto' ]; then + CASSANDRA_LISTEN_ADDRESS="$(hostname --ip-address)" + fi + + : ${CASSANDRA_BROADCAST_ADDRESS="$CASSANDRA_LISTEN_ADDRESS"} + + if [ "$CASSANDRA_BROADCAST_ADDRESS" = 'auto' ]; then + CASSANDRA_BROADCAST_ADDRESS="$(hostname --ip-address)" + fi + : ${CASSANDRA_BROADCAST_RPC_ADDRESS:=$CASSANDRA_BROADCAST_ADDRESS} + + if [ -n "${CASSANDRA_NAME:+1}" ]; then + : ${CASSANDRA_SEEDS:="cassandra"} + fi + : ${CASSANDRA_SEEDS:="$CASSANDRA_BROADCAST_ADDRESS"} + + sed -ri 's/(- seeds:).*/\1 "'"$CASSANDRA_SEEDS"'"/' "$CASSANDRA_CONFIG/cassandra.yaml" + + for yaml in \ + broadcast_address \ + broadcast_rpc_address \ + cluster_name \ + endpoint_snitch \ + listen_address \ + num_tokens \ + rpc_address \ + start_rpc \ + ; do + var="CASSANDRA_${yaml^^}" + val="${!var}" + if [ "$val" ]; then + sed -ri 's/^(# )?('"$yaml"':).*/\2 '"$val"'/' "$CASSANDRA_CONFIG/cassandra.yaml" + fi + done + + # Set the Client encryption options in the Cassandra configuration file + # + # grab the line number of the 'client_encryption_options' property then iterate down through the file until the + # first empty line is reached. This will be the end of the block containing all the properties for + # 'client_encryption_options'. + start_line_number=$(grep -n "client_encryption_options:" "$CASSANDRA_CONFIG/cassandra.yaml" | cut -d':' -f1) + count=${start_line_number} + line=$(sed "${count}q;d" "$CASSANDRA_CONFIG/cassandra.yaml") + + while [ "${line}" != "" ] + do + ((count++)) + line=$(sed "${count}q;d" "$CASSANDRA_CONFIG/cassandra.yaml") + done + + end_line_number=${count} + + for key_val in \ + "enabled:true" \ + "optional:false" \ + "keystore:\/etc\/ssl\/cassandra\-server\-keystore\.jks" \ + "keystore_password:${CASSANDRA_KEYSTORE_PASSWORD}" \ + "require_client_auth:true" \ + "truststore:\/etc\/ssl\/generic\-server\-truststore\.jks" \ + "truststore_password:${CASSANDRA_TRUSTSTORE_PASSWORD}" \ + "protocol:TLS" \ + "algorithm:SunX509" \ + "store_type:JKS" \ + "cipher_suites:[TLS_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA]" + do + property=$(echo ${key_val} | cut -d':' -f1) + value=$(echo ${key_val} | cut -d':' -f2) + sed -ie "${start_line_number},${end_line_number} s/\(#\ \)\{0,1\}\(${property}:\).*/\2 ${value}/" "$CASSANDRA_CONFIG/cassandra.yaml" + done + + # Set the Rack and DC properties + for rackdc in dc rack; do + var="CASSANDRA_${rackdc^^}" + val="${!var}" + if [ "$val" ]; then + sed -ri 's/^('"$rackdc"'=).*/\1 '"$val"'/' "$CASSANDRA_CONFIG/cassandra-rackdc.properties" + fi + done + + # Set JVM SSL encryption options for Cassandra + sed -ie 's/#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl=true"/JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl=true"/g' /etc/cassandra/cassandra-env.sh + sed -ie 's/#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"/JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"/g' /etc/cassandra/cassandra-env.sh + sed -ie 's/#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.protocols="/JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.protocols=TLSv1.2,TLSv1.1,TLSv1"/g' /etc/cassandra/cassandra-env.sh + sed -ie 's/#JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.cipher.suites="/JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.cipher.suites=TLS_RSA_WITH_AES_256_CBC_SHA"/g' /etc/cassandra/cassandra-env.sh + sed -ie "s/#JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.keyStore=\/path\/to\/keystore\"/JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.keyStore=\/etc\/ssl\/cassandra-server-keystore\.jks\"/g" /etc/cassandra/cassandra-env.sh + sed -ie "s/#JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.keyStorePassword=\"/JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.keyStorePassword=${CASSANDRA_KEYSTORE_PASSWORD}\"/g" /etc/cassandra/cassandra-env.sh + sed -ie "s/#JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.trustStore=\/path\/to\/truststore\"/JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.trustStore=\/etc\/ssl\/generic-server-truststore\.jks\"/g" /etc/cassandra/cassandra-env.sh + sed -ie "s/#JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.trustStorePassword=\"/JVM_OPTS=\"\$JVM_OPTS -Djavax.net.ssl.trustStorePassword=${CASSANDRA_TRUSTSTORE_PASSWORD}\"/g" /etc/cassandra/cassandra-env.sh +fi + +exec "$@" \ No newline at end of file diff --git a/src/packaging/docker-services/cassandra/cassandra.env b/src/packaging/docker-services/cassandra/cassandra.env index b13b2d63c..bbb15ddee 100644 --- a/src/packaging/docker-services/cassandra/cassandra.env +++ b/src/packaging/docker-services/cassandra/cassandra.env @@ -1,8 +1,5 @@ #!/usr/bin/env bash -# use a non-localhost listen address that matches the Docker Compose hostname -CASSANDRA_SEEDS=cassandra - # leave blank to auto configure CASSANDRA_BROADCAST_ADDRESS CASSANDRA_RPC_ADDRESS @@ -14,7 +11,7 @@ CASSANDRA_CLUSTER_NAME='reaper-cluster' CASSANDRA_NUM_TOKENS=32 # snitch information -CASSANDRA_DC=docker-compose-1 +CASSANDRA_DC=datacenter1 CASSANDRA_RACK=rack1 CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch diff --git a/src/packaging/docker-services/cassandra/jmxremote.access b/src/packaging/docker-services/cassandra/jmxremote.access index 7e72f323c..a6bb7beee 100644 --- a/src/packaging/docker-services/cassandra/jmxremote.access +++ b/src/packaging/docker-services/cassandra/jmxremote.access @@ -1 +1,2 @@ +cassandraUser readwrite reaperUser readwrite diff --git a/src/packaging/docker-services/cassandra/jmxremote.password b/src/packaging/docker-services/cassandra/jmxremote.password index 855702762..69e605781 100644 --- a/src/packaging/docker-services/cassandra/jmxremote.password +++ b/src/packaging/docker-services/cassandra/jmxremote.password @@ -1 +1,2 @@ +cassandraUser cassandraPass reaperUser reaperPass diff --git a/src/packaging/docker-services/cqlsh-initialize-reaper_db-ssl/cqlsh-initialize-reaper_db-ssl.env b/src/packaging/docker-services/cqlsh-initialize-reaper_db-ssl/cqlsh-initialize-reaper_db-ssl.env new file mode 100644 index 000000000..6ae7f1997 --- /dev/null +++ b/src/packaging/docker-services/cqlsh-initialize-reaper_db-ssl/cqlsh-initialize-reaper_db-ssl.env @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +CQLSH_ENABLE_SSL=true + +CASSANDRA_HOSTNAME=cassandra-ssl + +# use the same DC value set in the cassandra.env file +CASSANDRA_DC=datacenter1 + +CASSANDRA_KEYSTORE_ALIAS=cassandra +CASSANDRA_KEYSTORE_PASSWORD=keypassword \ No newline at end of file diff --git a/src/packaging/docker-services/cqlsh-initialize-reaper_db-ssl/docker-entrypoint.sh b/src/packaging/docker-services/cqlsh-initialize-reaper_db-ssl/docker-entrypoint.sh new file mode 100755 index 000000000..400734409 --- /dev/null +++ b/src/packaging/docker-services/cqlsh-initialize-reaper_db-ssl/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -xe + +REPLICATION_FACTOR=${1:-1} + +CQLSH_COMMAND="CREATE KEYSPACE IF NOT EXISTS reaper_db WITH replication = {'class': 'NetworkTopologyStrategy', '$CASSANDRA_DC': $REPLICATION_FACTOR };" + +/docker-entrypoint.sh \ No newline at end of file diff --git a/src/packaging/docker-services/cqlsh-initialize-reaper_db/cqlsh-initialize-reaper_db.env b/src/packaging/docker-services/cqlsh-initialize-reaper_db/cqlsh-initialize-reaper_db.env new file mode 100644 index 000000000..7b12e2e3b --- /dev/null +++ b/src/packaging/docker-services/cqlsh-initialize-reaper_db/cqlsh-initialize-reaper_db.env @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +CASSANDRA_HOSTNAME=cassandra + +# use the same DC value set in the cassandra.env file +CASSANDRA_DC=datacenter1 \ No newline at end of file diff --git a/src/packaging/docker-services/cqlsh-initialize-reaper_db/docker-entrypoint.sh b/src/packaging/docker-services/cqlsh-initialize-reaper_db/docker-entrypoint.sh new file mode 100755 index 000000000..400734409 --- /dev/null +++ b/src/packaging/docker-services/cqlsh-initialize-reaper_db/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -xe + +REPLICATION_FACTOR=${1:-1} + +CQLSH_COMMAND="CREATE KEYSPACE IF NOT EXISTS reaper_db WITH replication = {'class': 'NetworkTopologyStrategy', '$CASSANDRA_DC': $REPLICATION_FACTOR };" + +/docker-entrypoint.sh \ No newline at end of file diff --git a/src/packaging/docker-services/cqlsh-ssl/cqlsh-ssl.env b/src/packaging/docker-services/cqlsh-ssl/cqlsh-ssl.env new file mode 100644 index 000000000..cbac2440f --- /dev/null +++ b/src/packaging/docker-services/cqlsh-ssl/cqlsh-ssl.env @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +CQLSH_ENABLE_SSL=true + +CASSANDRA_HOSTNAME=cassandra-ssl + +CASSANDRA_KEYSTORE_ALIAS=cassandra +CASSANDRA_KEYSTORE_PASSWORD=keypassword \ No newline at end of file diff --git a/src/packaging/docker-services/initialize-reaper_db/Dockerfile b/src/packaging/docker-services/cqlsh/Dockerfile similarity index 64% rename from src/packaging/docker-services/initialize-reaper_db/Dockerfile rename to src/packaging/docker-services/cqlsh/Dockerfile index e8422d0a5..f59f58939 100644 --- a/src/packaging/docker-services/initialize-reaper_db/Dockerfile +++ b/src/packaging/docker-services/cqlsh/Dockerfile @@ -1,8 +1,12 @@ FROM cassandra:3.11 +ENV CQLSH_COMMAND="" \ + CQLSH_ENABLE_SSL=false \ + CASSANDRA_HOSTNAME="" + # create the reaper_db keyspace COPY docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"] # use a default replication factor of 1 -CMD ["1"] +CMD [""] \ No newline at end of file diff --git a/src/packaging/docker-services/cqlsh/cql.env b/src/packaging/docker-services/cqlsh/cql.env new file mode 100644 index 000000000..c9321df8e --- /dev/null +++ b/src/packaging/docker-services/cqlsh/cql.env @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +CASSANDRA_HOSTNAME=cassandra \ No newline at end of file diff --git a/src/packaging/docker-services/cqlsh/docker-entrypoint.sh b/src/packaging/docker-services/cqlsh/docker-entrypoint.sh new file mode 100755 index 000000000..ef299dddf --- /dev/null +++ b/src/packaging/docker-services/cqlsh/docker-entrypoint.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -xe + +CQLSH_OPTS="" + +# Check if SSL encryption is enabled +if [ "true" = "${CQLSH_ENABLE_SSL}" ]; then + CASSANDRA_KEYSTORE=/etc/ssl/cassandra-server-keystore.jks + PKCS12_KEYSTORE=/etc/ssl/cassandra-cql-keystore.p12 + CQL_CERT_PEM=/etc/ssl/cql-cer.pem + CQL_KEY_PEM=/etc/ssl/cql-key.pem + CQLSH_RC=.cqlshrc + + PKCS12_KEYSTORE_USERNAME=cassandra + PKCS12_KEYSTORE_PASSWORD=keypassword + + # convert cassandra's key store into the PKCS format + keytool -importkeystore \ + -srckeystore ${CASSANDRA_KEYSTORE} -srcalias ${CASSANDRA_KEYSTORE_ALIAS} \ + -srcstorepass ${CASSANDRA_KEYSTORE_PASSWORD} \ + -destkeystore ${PKCS12_KEYSTORE} -deststoretype PKCS12 -destalias ${PKCS12_KEYSTORE_USERNAME} \ + -deststorepass ${PKCS12_KEYSTORE_PASSWORD} + + # extract key and cert from the PKCS and place them in their own files + openssl pkcs12 -in ${PKCS12_KEYSTORE} -nokeys -out ${CQL_CERT_PEM} -passin pass:${PKCS12_KEYSTORE_PASSWORD} + openssl pkcs12 -in ${PKCS12_KEYSTORE} -nocerts -nodes -out ${CQL_KEY_PEM} -passin pass:${PKCS12_KEYSTORE_PASSWORD} + + # make cqlsh expect the generated key and cert files + cat <> ${CQLSH_RC} +[authentication] +username = ${PKCS12_KEYSTORE_USERNAME} +password = ${PKCS12_KEYSTORE_PASSWORD} + +[connection] +factory = cqlshlib.ssl.ssl_transport_factory + +[ssl] +certfile = /etc/ssl/ca-cert +validate = true +userkey = ${CQL_KEY_PEM} +usercert = ${CQL_CERT_PEM} +EOT + + cat ${CQLSH_RC} + + CQLSH_OPTS="--cqlshrc ${CQLSH_RC} --ssl" +fi + +# Check if an CQLSH command has been specified, if so run it and then exit. +if [ ! "${CQLSH_COMMAND}" = "" ] +then + cqlsh ${CASSANDRA_HOSTNAME} ${CQLSH_OPTS} -e "${CQLSH_COMMAND}" + exit +fi + +cqlsh ${CASSANDRA_HOSTNAME} ${CQLSH_OPTS} \ No newline at end of file diff --git a/src/packaging/docker-services/generate-ssl-stores/Dockerfile b/src/packaging/docker-services/generate-ssl-stores/Dockerfile new file mode 100644 index 000000000..5e2c30ffd --- /dev/null +++ b/src/packaging/docker-services/generate-ssl-stores/Dockerfile @@ -0,0 +1,16 @@ +FROM openjdk:8-jre-alpine + +ENV WORKDIR /usr/src/app +RUN mkdir -p ${WORKDIR} +WORKDIR ${WORKDIR} + +# Install openssl to generate and sign SSL certificates +RUN apk update \ + && apk add openssl + +# Add entrypoint script +COPY docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] + +# default passwords if no arguments supplied +CMD ["keypassword", "trustpassword"] \ No newline at end of file diff --git a/src/packaging/docker-services/generate-ssl-stores/docker-entrypoint.sh b/src/packaging/docker-services/generate-ssl-stores/docker-entrypoint.sh new file mode 100755 index 000000000..5098b1f5a --- /dev/null +++ b/src/packaging/docker-services/generate-ssl-stores/docker-entrypoint.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +set -ex + +# +# Generate the Key Stores and Trust Store to use when SSL encrypting communications between Cassandra and Reaper. In an +# SSL handshake, the purpose of a Truststore is to verify credentials and purpose of a Keystore is to provide the +# credentials. The credentials are derived from the Root Certificate Authority (Root CA). +# +# The Root CA that is generated from the Certificate Authority Configuration file is the core component of SSL +# encryption. The CA is used to sign other certificates, thus forming a certificate pair with the signed certificate. +# +# The Truststore contains the Root CA, and is used to determine whether the certificate from another party is to be +# trusted. That is, it is used to verify credentials from a third party. If the certificate from a third party were +# signed by the Root CA then the remote party can be trusted. +# +# The Keystore contains a certificate generated from the store and signed by the Root CA, and the Root CA used to sign +# the certificate. The Keystore determines which authentication certificate to send to the remote host and provide +# those when establishing the connection. +# + +CASSANDRA_KEYSTORE_PASSWORD=$1 +CASSANDRA_TRUSTSTORE_PASSWORD=$2 + +# +# Use three separate stores: +# - The Cassandra Keystore that will contain the Cassandra private certificate. +# - The Reaper Keystore that will contain the Reaper private certificate. +# - The Generic Truststore that is will contain the Root CA used to sign the private certificates in the +# Cassandra and Reaper Keystores. +# +CASSANDRA_KEYSTORE=${WORKDIR}/ssl-stores/cassandra-server-keystore.jks +REAPER_KEYSTORE=${WORKDIR}/ssl-stores/reaper-server-keystore.jks +GENERIC_TRUSTSTORE=${WORKDIR}/ssl-stores/generic-server-truststore.jks + +CA_CERT_CONFIG=${WORKDIR}/ca_cert.conf +ROOT_CA_CERT=${WORKDIR}/ssl-stores/ca-cert + +# Create the directory where the stores will go into if required. +mkdir -p ${WORKDIR}/ssl-stores/ + +# Check if there are any of the SSL stores exists and if so, prompt the user to delete them or exit +set +x +if [[ $(ls ${WORKDIR}/ssl-stores/*.jks | wc -l) -gt 0 ]] +then + echo + echo "WARNING: If any of the following stores exist, they will need to be deleted to proceed with the generation of new SSL stores." + echo " - ${CASSANDRA_KEYSTORE}" + echo " - ${REAPER_KEYSTORE}" + echo " - ${GENERIC_TRUSTSTORE}" + echo + while true + do + read -p "Do you wish to delete the above stores if they exist and continue with the generation of new SSL stores [Y/n]?" yn + case $yn in + [Yy]* ) break;; + [Nn]* ) exit;; + * ) echo "Please answer [Y]es or [n]o.";; + esac + done + +fi +set -x + +for store_name in ${CASSANDRA_KEYSTORE} ${REAPER_KEYSTORE} ${GENERIC_TRUSTSTORE} +do + rm -f ${store_name} +done + +set +x +echo +echo "Generic Certificate Authority configuration" +cat ${CA_CERT_CONFIG} +echo +set -x + +# Create the Root Certificate Authority (Root CA) from the Certificate Authority Configuration and verify contents. +openssl req -config ${CA_CERT_CONFIG} -new -x509 -keyout ca-key -out ${ROOT_CA_CERT} +openssl x509 -in ${ROOT_CA_CERT} -text -noout + +# Generate public/private key pair and the key stores. +keytool -genkeypair -keyalg RSA -alias cassandra \ + -keystore ${CASSANDRA_KEYSTORE} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -keysize 2048 \ + -dname "CN=node, OU=SSL-verification-cluster, O=TheLastPickle, C=AU" +keytool -genkeypair -keyalg RSA -alias reaper \ + -keystore ${REAPER_KEYSTORE} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -keysize 2048 \ + -dname "CN=reaper, OU=SSL-verification-cluster, O=TheLastPickle, C=AU" + +# Export certificates from key stores as a 'Signing Request' which the Root CA can then sign. +keytool -keystore ${CASSANDRA_KEYSTORE} -alias cassandra -certreq -file cassandra_cert_sr \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} +keytool -keystore ${REAPER_KEYSTORE} -alias reaper -certreq -file reaper_cert_sr \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} + +# Sign each of the certificates using the Root CA. +openssl x509 -req -CA ${ROOT_CA_CERT} -CAkey ca-key -in cassandra_cert_sr -out cassandra_cert_signed -CAcreateserial -passin pass:mypass +openssl x509 -req -CA ${ROOT_CA_CERT} -CAkey ca-key -in reaper_cert_sr -out reaper_cert_signed -CAcreateserial -passin pass:mypass + +# Import the the Root CA into the key stores. +keytool -keystore ${CASSANDRA_KEYSTORE} -alias CARoot -import -file ${ROOT_CA_CERT} -noprompt \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} +keytool -keystore ${REAPER_KEYSTORE} -alias CARoot -import -file ${ROOT_CA_CERT} -noprompt \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} + +# Import the signed certificates back into the key stores so that there is a complete chain. +keytool -keystore ${CASSANDRA_KEYSTORE} -alias cassandra -import -file cassandra_cert_signed \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} +keytool -keystore ${REAPER_KEYSTORE} -alias reaper -import -file reaper_cert_signed \ + -keypass ${CASSANDRA_KEYSTORE_PASSWORD} -storepass ${CASSANDRA_KEYSTORE_PASSWORD} + +# Create the trust store. +keytool -keystore ${GENERIC_TRUSTSTORE} -alias CARoot -importcert -file ${ROOT_CA_CERT} \ + -keypass ${CASSANDRA_TRUSTSTORE_PASSWORD} -storepass ${CASSANDRA_TRUSTSTORE_PASSWORD} -noprompt \ No newline at end of file diff --git a/src/packaging/docker-services/initialize-reaper_db/docker-entrypoint.sh b/src/packaging/docker-services/initialize-reaper_db/docker-entrypoint.sh deleted file mode 100755 index 6ac65d341..000000000 --- a/src/packaging/docker-services/initialize-reaper_db/docker-entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -x - -REPLICATION_FACTOR=$1 - -# create the required reaper_db keyspace to allow Reaper to store scheduling data -cqlsh cassandra -e \ - "CREATE KEYSPACE IF NOT EXISTS reaper_db \ - WITH replication = {'class': 'SimpleStrategy', \ - 'replication_factor': $REPLICATION_FACTOR };" diff --git a/src/packaging/docker-services/nodetool-ssl/nodetool-ssl.env b/src/packaging/docker-services/nodetool-ssl/nodetool-ssl.env new file mode 100644 index 000000000..b1ef90490 --- /dev/null +++ b/src/packaging/docker-services/nodetool-ssl/nodetool-ssl.env @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +CASSANDRA_HOSTNAME=cassandra-ssl + +NODETOOL_ENABLE_SSL=true + +CASSANDRA_KEYSTORE_PASSWORD=keypassword +CASSANDRA_TRUSTSTORE_PASSWORD=trustpassword \ No newline at end of file diff --git a/src/packaging/docker-services/nodetool/Dockerfile b/src/packaging/docker-services/nodetool/Dockerfile new file mode 100644 index 000000000..81a1d7076 --- /dev/null +++ b/src/packaging/docker-services/nodetool/Dockerfile @@ -0,0 +1,14 @@ +FROM cassandra:3.11 + +ENV WORKDIR /root +WORKDIR ${WORKDIR} + +ENV NODETOOL_ENABLE_SSL="false" \ + CASSANDRA_HOSTNAME="" + +COPY nodetool-ssl.properties ${WORKDIR}/.cassandra/nodetool-ssl.properties + +COPY docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD [""] \ No newline at end of file diff --git a/src/packaging/docker-services/nodetool/docker-entrypoint.sh b/src/packaging/docker-services/nodetool/docker-entrypoint.sh new file mode 100755 index 000000000..e9e5221dd --- /dev/null +++ b/src/packaging/docker-services/nodetool/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +NODETOOL_OPTS="" + +if [ "true" = "${NODETOOL_ENABLE_SSL}" ]; then + NODETOOL_OPTS="--ssl" + + sed -ie "s/CASSANDRA_KEYSTORE_PASSWORD/${CASSANDRA_KEYSTORE_PASSWORD}/g" ${WORKDIR}/.cassandra/nodetool-ssl.properties + sed -ie "s/CASSANDRA_TRUSTSTORE_PASSWORD/${CASSANDRA_TRUSTSTORE_PASSWORD}/g" ${WORKDIR}/.cassandra/nodetool-ssl.properties +fi + +nodetool --host ${CASSANDRA_HOSTNAME} --username cassandraUser --password cassandraPass ${NODETOOL_OPTS} $1 \ No newline at end of file diff --git a/src/packaging/docker-services/nodetool/nodetool-ssl.properties b/src/packaging/docker-services/nodetool/nodetool-ssl.properties new file mode 100644 index 000000000..e70e7e3af --- /dev/null +++ b/src/packaging/docker-services/nodetool/nodetool-ssl.properties @@ -0,0 +1,6 @@ +-Djavax.net.ssl.keyStore=/etc/ssl/cassandra-server-keystore.jks +-Djavax.net.ssl.keyStorePassword=CASSANDRA_KEYSTORE_PASSWORD +-Djavax.net.ssl.trustStore=/etc/ssl/generic-server-truststore.jks +-Djavax.net.ssl.trustStorePassword=CASSANDRA_TRUSTSTORE_PASSWORD +-Dcom.sun.management.jmxremote.ssl.need.client.auth=true +-Dcom.sun.management.jmxremote.registry.ssl=true \ No newline at end of file diff --git a/src/packaging/docker-services/nodetool/nodetool.env b/src/packaging/docker-services/nodetool/nodetool.env new file mode 100644 index 000000000..c9321df8e --- /dev/null +++ b/src/packaging/docker-services/nodetool/nodetool.env @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +CASSANDRA_HOSTNAME=cassandra \ No newline at end of file diff --git a/src/packaging/docker-services/reaper-ssl/reaper-ssl.env b/src/packaging/docker-services/reaper-ssl/reaper-ssl.env new file mode 100644 index 000000000..9479f60c2 --- /dev/null +++ b/src/packaging/docker-services/reaper-ssl/reaper-ssl.env @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +REAPER_CASS_CONTACT_POINTS=["cassandra-ssl"] + +# use SSL encryption when connecting to Cassandra via the native protocol +REAPER_CASS_NATIVE_PROTOCOL_SSL_ENCRYPTION_ENABLED=true + +# use SSL encryption when connecting to Cassandra via JMX +# use the passwords that match the generate-ssl-stores service defaults +JAVA_OPTS=-Dssl.enable=true -Djavax.net.ssl.keyStore=/etc/ssl/reaper-server-keystore.jks -Djavax.net.ssl.keyStorePassword=keypassword -Djavax.net.ssl.trustStore=/etc/ssl/generic-server-truststore.jks -Djavax.net.ssl.trustStorePassword=trustpassword \ No newline at end of file diff --git a/src/packaging/docker-services/reaper/reaper.env b/src/packaging/docker-services/reaper/reaper.env index 25bc15a7b..855ceec59 100644 --- a/src/packaging/docker-services/reaper/reaper.env +++ b/src/packaging/docker-services/reaper/reaper.env @@ -9,4 +9,4 @@ REAPER_JMX_PASSWORD=reaperPass REAPER_STORAGE_TYPE=cassandra REAPER_CASS_CLUSTER_NAME="reaper-cluster" REAPER_CASS_CONTACT_POINTS=["cassandra"] -REAPER_CASS_KEYSPACE=reaper_db +REAPER_CASS_KEYSPACE=reaper_db \ No newline at end of file diff --git a/src/packaging/resource/ca_cert.conf b/src/packaging/resource/ca_cert.conf new file mode 100644 index 000000000..4878385c1 --- /dev/null +++ b/src/packaging/resource/ca_cert.conf @@ -0,0 +1,14 @@ +[ req ] +distinguished_name = req_distinguished_name +prompt = no +output_password = mypass +default_bits = 2048 + +[ req_distinguished_name ] +C = AU +ST = NSW +L = Sydney +O = TLP +OU = TestCluster +CN = TestClusterMasterCA +emailAddress = no-reply@thelastpickle.com \ No newline at end of file diff --git a/src/server/pom.xml b/src/server/pom.xml index 22adcb626..3ef361de4 100755 --- a/src/server/pom.xml +++ b/src/server/pom.xml @@ -62,10 +62,10 @@ ${dropwizard.version} - org.coursera - dropwizard-metrics-datadog - ${dropwizard.metrics.datadog.version} - + org.coursera + dropwizard-metrics-datadog + ${dropwizard.metrics.datadog.version} + io.dropwizard dropwizard-assets diff --git a/src/server/src/main/docker/Dockerfile b/src/server/src/main/docker/Dockerfile index 03f0af4df..127538b52 100644 --- a/src/server/src/main/docker/Dockerfile +++ b/src/server/src/main/docker/Dockerfile @@ -13,6 +13,8 @@ ENV REAPER_SEGMENT_COUNT=200 \ REAPER_INCREMENTAL_REPAIR=false \ REAPER_DATACENTER_AVAILABILITY=ALL \ REAPER_ENABLE_DYNAMIC_SEEDS=true \ + REAPER_REPAIR_MANAGER_SCHEDULING_INTERVAL_SECONDS=30 \ + REAPER_ACTIVATE_QUERY_LOGGER=false \ REAPER_AUTO_SCHEDULE_ENABLED=false \ REAPER_AUTO_SCHEDULE_INITIAL_DELAY_PERIOD=PT15S \ REAPER_AUTO_SCHEDULE_PERIOD_BETWEEN_POLLS=PT10M \ @@ -35,21 +37,35 @@ ENV REAPER_SEGMENT_COUNT=200 \ REAPER_CASS_AUTH_ENABLED="false" \ REAPER_CASS_AUTH_USERNAME="cassandra" \ REAPER_CASS_AUTH_PASSWORD="cassandra" \ + REAPER_CASS_NATIVE_PROTOCOL_SSL_ENCRYPTION_ENABLED="false" \ REAPER_DB_DRIVER_CLASS="org.h2.Driver" \ REAPER_DB_URL="jdbc:h2:/var/lib/cassandra-reaper/db;MODE=PostgreSQL" \ REAPER_DB_USERNAME="" \ REAPER_DB_PASSWORD="" \ + REAPER_METRICS_ENABLED=false \ + REAPER_METRICS_FREQUENCY="1 minute" \ + REAPER_METRICS_REPORTERS="[]" \ JAVA_OPTS="" ADD cassandra-reaper.yml /etc/cassandra-reaper.yml ADD entrypoint.sh /usr/local/bin/entrypoint.sh -ADD append-persistence.sh /usr/local/bin/append-persistence.sh +ADD configure-persistence.sh /usr/local/bin/configure-persistence.sh +ADD configure-metrics.sh /usr/local/bin/configure-metrics.sh + RUN addgroup -S reaper && \ adduser -S reaper reaper && \ apk add --no-cache 'su-exec>=0.2' && \ mkdir -p /var/lib/cassandra-reaper && \ - chown reaper:reaper /etc/cassandra-reaper.yml /var/lib/cassandra-reaper /usr/local/bin/entrypoint.sh /usr/local/bin/append-persistence.sh && \ - chmod u+x /usr/local/bin/entrypoint.sh /usr/local/bin/append-persistence.sh + chown reaper:reaper \ + /etc/cassandra-reaper.yml \ + /var/lib/cassandra-reaper \ + /usr/local/bin/entrypoint.sh \ + /usr/local/bin/configure-persistence.sh \ + /usr/local/bin/configure-metrics.sh && \ + chmod u+x \ + /usr/local/bin/entrypoint.sh \ + /usr/local/bin/configure-persistence.sh \ + /usr/local/bin/configure-metrics.sh VOLUME /var/lib/cassandra-reaper diff --git a/src/server/src/main/docker/cassandra-reaper.yml b/src/server/src/main/docker/cassandra-reaper.yml index 6c12f09a6..eb51a8124 100644 --- a/src/server/src/main/docker/cassandra-reaper.yml +++ b/src/server/src/main/docker/cassandra-reaper.yml @@ -9,6 +9,7 @@ storageType: ${REAPER_STORAGE_TYPE} enableCrossOrigin: ${REAPER_ENABLE_CORS} incrementalRepair: ${REAPER_INCREMENTAL_REPAIR} enableDynamicSeedList: ${REAPER_ENABLE_DYNAMIC_SEEDS} +repairManagerSchedulingIntervalSeconds: ${REAPER_REPAIR_MANAGER_SCHEDULING_INTERVAL_SECONDS} # datacenterAvailability has three possible values: ALL | LOCAL | EACH # the correct value to use depends on whether jmx ports to C* nodes in remote datacenters are accessible diff --git a/src/server/src/main/docker/configure-metrics.sh b/src/server/src/main/docker/configure-metrics.sh new file mode 100644 index 000000000..844c00aac --- /dev/null +++ b/src/server/src/main/docker/configure-metrics.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ "true" = "${REAPER_METRICS_ENABLED}" ]; then +cat <> /etc/cassandra-reaper.yml +metrics: + frequency: ${REAPER_METRICS_FREQUENCY} + reporters: ${REAPER_METRICS_REPORTERS} +EOT +fi \ No newline at end of file diff --git a/src/server/src/main/docker/append-persistence.sh b/src/server/src/main/docker/configure-persistence.sh similarity index 65% rename from src/server/src/main/docker/append-persistence.sh rename to src/server/src/main/docker/configure-persistence.sh index c1eef2f44..82797069f 100644 --- a/src/server/src/main/docker/append-persistence.sh +++ b/src/server/src/main/docker/configure-persistence.sh @@ -2,7 +2,11 @@ case ${REAPER_STORAGE_TYPE} in "cassandra") + +# BEGIN cassandra persistence options cat <> /etc/cassandra-reaper.yml +activateQueryLogger: ${REAPER_ACTIVATE_QUERY_LOGGER} + cassandra: clusterName: ${REAPER_CASS_CLUSTER_NAME} contactPoints: ${REAPER_CASS_CONTACT_POINTS} @@ -17,8 +21,19 @@ cat <> /etc/cassandra-reaper.yml password: ${REAPER_CASS_AUTH_PASSWORD} EOT fi + +if [ "true" = "${REAPER_CASS_NATIVE_PROTOCOL_SSL_ENCRYPTION_ENABLED}" ]; then +cat <> /etc/cassandra-reaper.yml + ssl: + type: jdk +EOT +fi +# END cassandra persistence options + ;; "database") + +# BEGIN database persistence options cat <> /etc/cassandra-reaper.yml database: driverClass: ${REAPER_DB_DRIVER_CLASS} @@ -26,5 +41,6 @@ database: user: ${REAPER_DB_USERNAME} password: ${REAPER_DB_PASSWORD} EOT +# END database persistence options esac diff --git a/src/server/src/main/docker/entrypoint.sh b/src/server/src/main/docker/entrypoint.sh index 3db3e3ddb..0117abd15 100644 --- a/src/server/src/main/docker/entrypoint.sh +++ b/src/server/src/main/docker/entrypoint.sh @@ -2,9 +2,12 @@ if [ "$1" = 'cassandra-reaper' ]; then set -x - su-exec reaper /usr/local/bin/append-persistence.sh - exec su-exec reaper java -jar ${JAVA_OPTS} \ - /usr/local/lib/cassandra-reaper.jar server /etc/cassandra-reaper.yml + su-exec reaper /usr/local/bin/configure-persistence.sh + su-exec reaper /usr/local/bin/configure-metrics.sh + exec su-exec reaper java \ + ${JAVA_OPTS} \ + -jar /usr/local/lib/cassandra-reaper.jar server \ + /etc/cassandra-reaper.yml fi exec "$@"