Skip to content

Commit

Permalink
feat: initial version (#1)
Browse files Browse the repository at this point in the history
# Description

What is the overall goal of your PR? Which problem does it solve? Please
also include relevant motivation and context.
List any dependencies that are required for this change.

Fixes #(issue number)

# Migrations required

yes: please describe the migration
no: please delete the whole paragraph

# Verification

Please describe the test cases you used to verify your code. Did you
check the change in your environment?

# Checklist

- [ ] My code follows the style guidelines of the project
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
  • Loading branch information
kayman-mk authored Jan 8, 2025
1 parent 45f1a2d commit b34ba71
Show file tree
Hide file tree
Showing 29 changed files with 715 additions and 1 deletion.
11 changes: 11 additions & 0 deletions .config/dictionaries/project.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
abstractmethod
awscurrent
awspending
boto
classmethod
localstack
mypy
powertools
pydantic
pytest
rotatation
9 changes: 9 additions & 0 deletions .config/dictionaries/python.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dists
doctest
hoverkraft
junitxml
pycache
pypa
pypi
pytest
setuptools
53 changes: 53 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: Publish Python 🐍 distribution 📦 to PyPI

# yamllint disable-line rule:truthy
on:
release:
types: [published]

jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- name: Set up Python 3.10
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: "3.10"
cache: 'pip'
cache-dependency-path: setup.cfg
- name: Install pypa/build
run: >-
python3 -m pip install build --user
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: python-package-distributions
path: dist/

publish-to-pypi:
name: >-
Publish Python 🐍 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/rds-proxy-password-rotation # Replace <package-name> with your PyPI project name
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
69 changes: 69 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python build

# yamllint disable-line rule:truthy
on:
pull_request:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: setup.cfg

- name: Install dependencies
run: |
python -m pip install -e "."
- uses: astral-sh/ruff-action@e6390afda04da2e9ef69fe1e2ae0264164550c21 # v3.0.1
name: Lint on ${{ matrix.python-version }}
with:
args: "check"
# renovate: datasource=github-releases depName=astral-sh/ruff
version: "0.8.6"

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: setup.cfg

- name: Install dependencies
run: |
python -m pip install -e ".[test]"
- name: Run docker-compose
uses: hoverkraft-tech/[email protected]
with:
compose-file: "./tests/docker-compose.yml"

- name: Test on ${{ matrix.python-version }}
run: |
pytest -v --doctest-modules --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.egg-info
__pycache__/
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/aws.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/git_toolbox_blame.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/git_toolbox_prj.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions .idea/rds-proxy-password-rotation.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,54 @@
# rds-proxy-password-rotation
Python script to rotate the RDS credentials using a separate credential for the application.

:warning: **Work in progress** :warning:

- implement step CREATE_SECRET
- implement step SET_SECRET
- implement step TEST_SECRET
- implement step FINISH_SECRET
- add Terraform module

Python script for multi-user password rotation using RDS and RDS proxy. It supports a separate credential for the application.

## Pre-requisites

1. Python 3.10 or later
2. For each db user:
1. Create a secret in AWS Secrets Manager with the following key-value pairs:
- `username`: The username for the user
- `password`: The password for the user
This credential will be used by the application to connect to the proxy. You may add additional key-value pairs as needed.
2. Clone the user in the database and grant the necessary permissions. We suggest to add a `-clone` suffix to the username.
3. Create two secrets (for the original user and the clone) in AWS Secrets Manager with the following key-value pairs:
- `username`: The username for the user
- `password`: The password for the user
These credentials are used by the proxy to connect to the database. You may add additional key-value pairs as needed.

## Architecture

![Architecture](assets/architecture.png)

## Challenges with RDS and RDS Proxy

RDS Proxy is a fully managed, highly available database proxy for Amazon Relational Database Service (RDS) that makes applications
more scalable, more resilient to database failures, and more secure. It allows applications to pool and share database connections
to improve efficiency and reduce the load on your database instances.

However, RDS Proxy does not support multi-user password rotation out of the box. This script provides a solution to this problem.

Using an RDS Proxy requires a secret in AWS Secrets Manager with the credentials to connect to the database. This secret is used by
the proxy to connect to the database. The proxy allows the application to connect to the database using the same credentials and
then forwards the requests to the database with the same credentials. This means that the credentials in the secret must be valid
in the database at all times. But what if you want to rotate the password for the user that the proxy uses to connect to the
database? You can’t just update the secret in SecretsManager because the proxy will stop working as soon as the secret is updated.
And you can’t just update the password in the database because the proxy will stop working as soon as the password is updated.

## Why password rotation is a good practice

Password rotation is a good idea for several reasons:

1. **Enhanced Security**: Regularly changing passwords reduces the risk of unauthorized access due to compromised credentials.
2. **Mitigates Risk**: Limits the time window an attacker has to exploit a stolen password.
3. **Compliance**: Many regulatory standards and security policies require periodic password changes.
4. **Reduces Impact of Breaches**: If a password is compromised, rotating it ensures that the compromised password is no longer valid.
5. **Encourages Good Practices**: Promotes the use of strong, unique passwords and discourages password reuse.
Binary file added assets/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ['setuptools==75.6.0']
build-backend = 'setuptools.build_meta'
40 changes: 40 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[metadata]
name = rds-proxy-password-rotatation
version = 1.0.0
author = Hapag-Lloyd AG
author_email = [email protected]
description = A program to rotate the password of an RDS database accessed via a RDS proxy
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/Hapag-Lloyd/rds-proxy-password-rotation
project_urls =
Bug Tracker = https://github.com/Hapag-Lloyd/rds-proxy-password-rotation/issues
repository = https://github.com/Hapag-Lloyd/rds-proxy-password-rotation
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent

[options]
package_dir =
= src
packages = find:
python_requires = >=3.10
install_requires =
aws-lambda-powertools==3.4.0
boto3==1.35.83
boto3-stubs[secretsmanager]==1.35.83
dependency-injector==4.44.0
pydantic==2.10.4

[options.extras_require]
test =
pytest==8.3.4
pytest-cov==6.0.0
uuid==1.30

# updates an outdated dependency of local setup
nose==1.3.7

[options.packages.find]
where = src
27 changes: 27 additions & 0 deletions src/rds_proxy_password_rotatation/adapter/aws_lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from dependency_injector.wiring import inject, Provide

from aws_lambda_powertools.utilities.parser import event_parser
from aws_lambda_powertools.utilities.typing import LambdaContext

from rds_proxy_password_rotatation.adapter.aws_lambda_function_model import AwsSecretManagerRotationEvent
from rds_proxy_password_rotatation.adapter.container import Container
from rds_proxy_password_rotatation.password_rotation_application import PasswordRotationApplication


container = None

@event_parser(model=AwsSecretManagerRotationEvent)
def lambda_handler(event: AwsSecretManagerRotationEvent, context: LambdaContext) -> None:
global container

if container is None:
container = Container()
container.config.api_key.from_env("API_KEY", required=True)
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
container.wire(modules=[__name__])

__call_application(event)

@inject
def __call_application(event: AwsSecretManagerRotationEvent, application: PasswordRotationApplication = Provide[Container.password_rotation_application]) -> None:
application.rotate_secret(event.step.to_rotation_step(), event.secret_id)
Loading

0 comments on commit b34ba71

Please sign in to comment.