Skip to content

Commit

Permalink
Merge pull request #571 from abinoam/aws_secrets
Browse files Browse the repository at this point in the history
Getting credentials at runtime from AWS Secrets Manager
  • Loading branch information
jcormier authored Dec 5, 2024
2 parents baa1cc5 + cfe24b8 commit 458b6b3
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 14 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ruby:3.2-slim-bookworm
FROM ruby:3.3-slim-bookworm

LABEL maintainer="[email protected]"

Expand All @@ -21,7 +21,7 @@ RUN apt-get update \
imagemagick subversion git cvs bzr mercurial darcs rsync locales openssh-client \
gcc g++ make patch pkg-config gettext-base libc6-dev zlib1g-dev libxml2-dev \
default-libmysqlclient-dev libmariadb-dev libpq5 libyaml-0-2 libcurl4 libssl3 uuid-dev xz-utils \
libxslt1.1 libffi8 zlib1g gsfonts vim-tiny ghostscript sqlite3 libsqlite3-dev \
libxslt1.1 libffi8 zlib1g gsfonts vim-tiny ghostscript sqlite3 libsqlite3-dev jq\
&& update-locale LANG=C.UTF-8 LC_MESSAGES=POSIX \
&& gem install --no-document bundler \
&& rm -rf /var/lib/apt/lists/*
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- [PostgreSQL](#postgresql)
- [External PostgreSQL Server](#external-postgresql-server)
- [Linking to PostgreSQL Container](#linking-to-postgresql-container)
- [AWS RDS Integration](#aws-rds-integration)
- [Memcached (Optional)](#memcached-optional)
- [External Memcached Server](#external-memcached-server)
- [Linking to Memcached Container](#linking-to-memcached-container)
Expand Down Expand Up @@ -377,6 +378,10 @@ Here the image will also automatically fetch the `DB_NAME`, `DB_USER` and `DB_PA
- [orchardup/postgresql](https://hub.docker.com/r/orchardup/postgresql/)
- [paintedfox/postgresql](https://hub.docker.com/r/paintedfox/postgresql/)

### AWS RDS Integration
**docker-redmine** has support for fetching secrets from AWS Secrets Manager at runtime.
Read [docs/aws.md](docs/aws.md) for detailed instructions.

## Memcached (Optional)

This image can (optionally) be configured to use a memcached server to speed up Redmine. This is particularly useful when you have a large number users.
Expand Down
10 changes: 0 additions & 10 deletions assets/runtime/env-defaults
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,6 @@ case ${DB_TYPE} in
sqlite3) DB_ADAPTER=${DB_ADAPTER:-sqlite3} ;;
esac

if [[ ! -z "${DB_SSL_MODE}" ]]
then
# Add sslmode or ssl_mode depending on type
case ${DB_ADAPTER} in
mysql2) DB_SSL_MODE="ssl_mode: ${DB_SSL_MODE}" ;;
postgresql) DB_SSL_MODE="sslmode: ${DB_SSL_MODE}" ;;
sqlite3) DB_SSL_MODE= ;; # No ssl
esac
fi

## UNICORN (deprecated)
UNICORN_WORKERS=${UNICORN_WORKERS:-2}
UNICORN_TIMEOUT=${UNICORN_TIMEOUT:-60}
Expand Down
54 changes: 52 additions & 2 deletions assets/runtime/functions
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,50 @@ update_template() {
}

redmine_finalize_database_parameters() {
if [[ -n ${AWS_DB_CREDENTIALS_SECRET_NAME} ]]; then
# Set the region, prioritizing a specific value if available
local REGION=${AWS_DB_CREDENTIALS_SECRET_REGION:-${AWS_REGION}}

echo "Installing aws-secrets-manager Ruby gem..."
gem install aws-secrets-manager --no-document --silent

echo "Fetching credentials from AWS Secrets Manager using get_aws_secret.rb..."
local SECRET_STRING=$(ruby ${REDMINE_RUNTIME_ASSETS_DIR}/get_aws_secret.rb $AWS_DB_CREDENTIALS_SECRET_NAME $REGION)

# Check if the command was successful
if [[ $? -ne 0 || -z "${SECRET_STRING}" ]]; then
echo "Failed to retrieve credentials from Secrets Manager. Please ensure that permissions and settings are correctly configured."
return 1
fi

# Parse the JSON from the SecretString
echo "Parsing received credentials..."
DB_USER=$(echo "${SECRET_STRING}" | jq -r '.username')
DB_PASS=$(echo "${SECRET_STRING}" | jq -r '.password')
DB_ADAPTER=$(echo "${SECRET_STRING}" | jq -r '.engine')
DB_HOST=$(echo "${SECRET_STRING}" | jq -r '.host')
DB_PORT=$(echo "${SECRET_STRING}" | jq -r '.port')

# Set default encoding and adapter based on the engine
case ${DB_ADAPTER} in
postgres | postgresql)
DB_ADAPTER=postgresql
DB_ENCODING=${DB_ENCODING:-unicode}
;;
mysql | mysql2)
DB_ADAPTER=mysql2
DB_ENCODING=${DB_ENCODING:-utf8}
;;
*)
echo "Error: Unsupported engine '${DB_ADAPTER}'. Only 'postgresql' and 'mysql2' are accepted. Aborting..."
return 1
;;
esac

# is a mysql or postgresql database linked?
# requires that the mysql or postgresql containers have exposed
# port 3306 and 5432 respectively.
if [[ -n ${MYSQL_PORT_3306_TCP_ADDR} ]]; then
elif [[ -n ${MYSQL_PORT_3306_TCP_ADDR} ]]; then
DB_ADAPTER=${DB_ADAPTER:-mysql2}
DB_HOST=${DB_HOST:-mysql}
DB_PORT=${DB_PORT:-${MYSQL_PORT_3306_TCP_PORT}}
Expand Down Expand Up @@ -211,6 +251,16 @@ redmine_finalize_database_parameters() {
;;
esac

if [[ ! -z "${DB_SSL_MODE}" ]]
then
# Add sslmode or ssl_mode depending on type
case ${DB_ADAPTER} in
mysql2) DB_SSL_MODE="ssl_mode: ${DB_SSL_MODE}" ;;
postgresql) DB_SSL_MODE="sslmode: ${DB_SSL_MODE}" ;;
sqlite3) DB_SSL_MODE= ;; # No ssl
esac
fi

# set default user and database
DB_USER=${DB_USER:-root}
DB_NAME=${DB_NAME:-redmine_production}
Expand Down Expand Up @@ -286,7 +336,7 @@ redmine_check_mysql_database_tx_isolation() {
}

redmine_configure_database() {
echo -n "Configuring redmine::database"
echo "Configuring redmine::database"

redmine_finalize_database_parameters
redmine_check_database_connection
Expand Down
21 changes: 21 additions & 0 deletions assets/runtime/get_aws_secret.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env ruby

require 'aws-sdk-secretsmanager'

# Secret name in AWS Secrets Manager
secret_name = ARGV[0]
region_name = ARGV[1]

raise "Usage: #{__FILE__} <secret_name> <region_name>" unless ARGV[0] && ARGV[1]

# Retrieve credentials from Secrets Manager
begin
client = Aws::SecretsManager::Client.new(region: region_name)
secret_value = client.get_secret_value(secret_id: secret_name)

# Extract the credentials
puts secret_value.secret_string
rescue Aws::SecretsManager::Errors::ServiceError => e
puts "Error accessing Secrets Manager: #{e.message}"
exit 1
end
49 changes: 49 additions & 0 deletions docker-compose-aws.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
services:
redmine:
build: ./
image: sameersbn/redmine:6.0.1
environment:
- TZ=America/Sao_Paulo
- AWS_DB_CREDENTIALS_SECRET_REGION=
- AWS_DB_CREDENTIALS_SECRET_NAME=
- DB_NAME=redmine
- DB_SSL_MODE=prefer
- DB_CREATE=false

- REDMINE_PORT=10083
- REDMINE_HTTPS=false
- REDMINE_RELATIVE_URL_ROOT=
- REDMINE_SECRET_TOKEN=

- REDMINE_SUDO_MODE_ENABLED=false
- REDMINE_SUDO_MODE_TIMEOUT=15

- REDMINE_CONCURRENT_UPLOADS=2

- REDMINE_BACKUP_SCHEDULE=
- REDMINE_BACKUP_EXPIRY=
- REDMINE_BACKUP_TIME=

- SMTP_ENABLED=false
- SMTP_METHOD=smtp
- SMTP_DOMAIN=www.example.com
- SMTP_HOST=smtp.gmail.com
- SMTP_PORT=587
- [email protected]
- SMTP_PASS=password
- SMTP_STARTTLS=true
- SMTP_AUTHENTICATION=:login

- IMAP_ENABLED=false
- IMAP_HOST=imap.gmail.com
- IMAP_PORT=993
- [email protected]
- IMAP_PASS=password
- IMAP_SSL=true
- IMAP_INTERVAL=30

ports:
- "10083:80"
volumes:
- /srv/docker/redmine/redmine:/home/redmine/data
- /srv/docker/redmine/redmine-logs:/var/log/redmine
109 changes: 109 additions & 0 deletions docs/aws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# AWS RDS Integration

This feature allows Redmine to connect to an AWS RDS PostgreSQL database using credentials dynamically retrieved from AWS Secrets Manager.


## **Configuration**

Use [docker-compose-aws.yml](../docker-compose-aws.yml) as a starting point and set the following environment variables under the `redmine` service:

```yaml
services:
redmine:
environment:
- AWS_DB_CREDENTIALS_SECRET_REGION=<AWS_REGION>
- AWS_DB_CREDENTIALS_SECRET_NAME=<SECRET_NAME_OR_ARN>
- DB_NAME=redmine
- DB_CREATE=false
```
- **`AWS_DB_CREDENTIALS_SECRET_REGION`**: Specify the AWS region where the secret is stored (e.g., `us-east-1`).
- **`AWS_DB_CREDENTIALS_SECRET_NAME`**: Provide the name or ARN of the secret containing the database credentials.
- **`DB_NAME`**: Specifies the database name.
- **`DB_CREATE`**: Prevents Redmine from trying to create or overwrite the database during startup.

**Note**:
Unlike other setups, there is no PostgreSQL companion container running alongside Redmine.
You will connect directly to an AWS RDS PostgreSQL instance.

## **AWS Secrets Manager Setup**

To store the database credentials securely, create a secret in AWS Secrets Manager.
Use the following steps:

1. Log in to the **AWS Management Console** and navigate to **Secrets Manager**.
2. Click **Store a new secret** and select **Credentials for Amazon RDS database** as the secret type.
3. Enter your database credentials (username and password).
4. Select your RDS database instance from the list.
5. Configure the secret name (e.g., `redmine_aws_rds_credentials`) and save the secret.

If you selected "Credentials for Amazon RDS database" the secret will have the following fields, at least:
* engine (it will be translated into "adapter" (eg: postgresql, mysql))
* username
* password
* host
* port

## **Database Preparation**

Before starting the container:

1. Create the PostgreSQL role and database on your RDS instance manually. Use the same process as for a PostgreSQL companion container as in [External PostgreSQL Server](#external-postgresql-server):

```sql
CREATE ROLE redmine with LOGIN CREATEDB PASSWORD 'password';
CREATE DATABASE redmine_production;
GRANT ALL PRIVILEGES ON DATABASE redmine_production to redmine;
```

2. Ensure the docker-compose.yml file `DB_CREATE=false` environment variable is set to avoid Redmine overwriting the database.

## **IAM Permissions and EC2 Configuration**

Ensure that:
1. The EC2 instance running the Redmine container has the appropriate IAM role attached. This role should grant:
- Access to read the secret from AWS Secrets Manager.
- Network access to the RDS instance.

Example IAM policy for accessing the secret:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:<AWS_REGION>:<AWS_ACCOUNT_ID>:secret:<SECRET_NAME>"
}
]
}
```

2. Security group rules are configured to allow connections from the EC2 instance to the RDS instance.

## **How It Works**

When the container starts:
1. It fetches the database credentials from AWS Secrets Manager using the provided `AWS_DB_CREDENTIALS_SECRET_REGION` and `AWS_DB_CREDENTIALS_SECRET_NAME`.
2. These credentials are used to establish a connection to the AWS RDS PostgreSQL instance.

This approach eliminates hardcoding sensitive credentials in the `docker-compose.yml` file, enhancing security and flexibility.

Note: Although the credentials are not hardcoded in the `docker-compose.yml` file, during runtime, they will be written to the config/database.yml file inside the container, just like in the other docker-compose template examples provided by this image.

## **Debugging**

You can check if your AWS EC2 instance is properly configured to have access to get the secrets by using the same script used inside the container.

```bash
gem install aws-sdk-secretsmanager
ruby assets/runtime/get_aws_secret.rb secret_name us-east-1
```

With the received credentials you may try to connect to AWS RDS instance and see if the AWS EC2 instance is allowed to do so.

```bash
psql -h host -U username -d database
```

Check AWS official documentation for more detailed information.

0 comments on commit 458b6b3

Please sign in to comment.