-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
# 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
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
dists | ||
doctest | ||
hoverkraft | ||
junitxml | ||
pycache | ||
pypa | ||
pypi | ||
pytest | ||
setuptools |
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 |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
*.egg-info | ||
__pycache__/ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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. |
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' |
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 |
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) |