Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting credentials at runtime from AWS Secrets Manager #571

Merged
merged 7 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading