Skip to content
Open
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
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,28 @@ database_engine: POSTGRESQL
database_engine: MYSQL
```

**IAM Auth**
**User Authentication**

IAM authentication to the proxy is enabled by default, to disable this use the following config
RDS proxy takes a list of users that can access the database through the proxy.

Each user requires a secret in AWS Secrets Manager which contains the username and password in a json format.
The Secrets Manager ARN is required to be passed through as a parameter to the component. [Setting up database credentials in Secrets Manager](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-setup.html#rds-proxy-secrets-arns)

```json
{"username":"admin","password":"choose_your_own_password"}
```

A user is enabled by default with IAM authentication enabled. This user can be [disabled](tests/multiple_users.test.yaml) or [added](tests/multiple_users.test.yaml) onto.

```yaml
iam_auth: DISABLED
users:
default:
secret_arn_parameter: SecretCredentials
iam_auth: REQUIRED # REQUIRED | DISABLED
```

IAM auth can also be [disabled](tests/disable_iam_auth.test.yaml) on the default user

**Security Group Rules**

configure network access to the proxy, set a ingress rule on the security group. For further rule options see docs [here](https://github.com/theonestack/hl-component-lib-ec2#security-group-rules)
Expand Down
8 changes: 7 additions & 1 deletion rds-proxy.cfhighlander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
ComponentParam 'DBClusterSecurityGroup', type: 'AWS::EC2::SecurityGroup::Id'

ComponentParam 'ProxyName', 'rdsproxy', description: 'name of the rds proxy required by cloudformation. this value prefixed with the environment name'
ComponentParam 'SecretCredentials', description: 'secrets manager arn of the secret. format of the secret must be json {"username": "user", "password": "pass"}'

users.each do |name, config|
if config.has_key?('secret_arn_parameter')
ComponentParam config['secret_arn_parameter'],
description: "#{name} RDS username and password Secrets Manager ARN. format of the secret must be json {\"username\": \"user\", \"password\": \"pass\"}"
end
end

ComponentParam 'IdleClientTimeout', 120, type: 'Number',
description: 'proxy idle connection timeout in seconds'
Expand Down
31 changes: 24 additions & 7 deletions rds-proxy.cfndsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,31 @@
GroupId Ref(:DBClusterSecurityGroup)
}

auth_config = []
external_parameters[:users].each do |name,config|
if config.has_key?('disabled') && config['disabled']
next
end

user = {
AuthScheme: 'SECRETS',
IAMAuth: config.fetch('iam_auth', 'REQUIRED'),
SecretArn: Ref(config['secret_arn_parameter'])
}

if config.has_key?('description')
user[:Description] = config['description']
end

if config.has_key?('username')
user[:UserName ] = config['username']
end

auth_config << user
end

RDS_DBProxy(:RdsProxy) {
Auth([
{
AuthScheme: 'SECRETS',
IAMAuth: external_parameters[:iam_auth],
SecretArn: Ref(:SecretCredentials)
}
])
Auth auth_config
EngineFamily database_engine
IdleClientTimeout Ref(:IdleClientTimeout)
RequireTLS Ref(:RequireTLS)
Expand Down
8 changes: 6 additions & 2 deletions rds-proxy.config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
database_engine: MYSQL # POSTGRESQL | MYSQL
iam_auth: REQUIRED # REQUIRED | DISABLED

dns_format: ${EnvironmentName}.${DnsDomain}
hostname: postgres-proxy
hostname: postgres-proxy

users:
default:
secret_arn_parameter: SecretCredentials
iam_auth: REQUIRED # REQUIRED | DISABLED
184 changes: 184 additions & 0 deletions spec/multiple_users_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
require 'yaml'

describe 'compiled component rds-proxy' do

context 'cftest' do
it 'compiles test' do
expect(system("cfhighlander cftest #{@validate} --tests tests/multiple_users.test.yaml")).to be_truthy
end
end

let(:template) { YAML.load_file("#{File.dirname(__FILE__)}/../out/tests/multiple_users/rds-proxy.compiled.yaml") }

context "Resource" do


context "SecurityGroup" do
let(:resource) { template["Resources"]["SecurityGroup"] }

it "is of type AWS::EC2::SecurityGroup" do
expect(resource["Type"]).to eq("AWS::EC2::SecurityGroup")
end

it "to have property VpcId" do
expect(resource["Properties"]["VpcId"]).to eq({"Ref"=>"VPCId"})
end

it "to have property GroupDescription" do
expect(resource["Properties"]["GroupDescription"]).to eq({"Fn::Join"=>[" ", [{"Ref"=>"EnvironmentName"}, "rds-proxy", "security group"]]})
end

it "to have property Tags" do
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Name", "Value"=>{"Fn::Sub"=>"${EnvironmentName}-rds-proxy"}}, {"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}, {"Key"=>"EnvironmentType", "Value"=>{"Ref"=>"EnvironmentType"}}])
end

end

context "ProxyPortAccessToDBCluster" do
let(:resource) { template["Resources"]["ProxyPortAccessToDBCluster"] }

it "is of type AWS::EC2::SecurityGroupIngress" do
expect(resource["Type"]).to eq("AWS::EC2::SecurityGroupIngress")
end

it "to have property IpProtocol" do
expect(resource["Properties"]["IpProtocol"]).to eq("tcp")
end

it "to have property FromPort" do
expect(resource["Properties"]["FromPort"]).to eq({"Ref"=>"TargetDBClusterPort"})
end

it "to have property ToPort" do
expect(resource["Properties"]["ToPort"]).to eq({"Ref"=>"TargetDBClusterPort"})
end

it "to have property SourceSecurityGroupId" do
expect(resource["Properties"]["SourceSecurityGroupId"]).to eq({"Fn::GetAtt"=>["SecurityGroup", "GroupId"]})
end

it "to have property GroupId" do
expect(resource["Properties"]["GroupId"]).to eq({"Ref"=>"DBClusterSecurityGroup"})
end

end

context "RdsProxy" do
let(:resource) { template["Resources"]["RdsProxy"] }

it "is of type AWS::RDS::DBProxy" do
expect(resource["Type"]).to eq("AWS::RDS::DBProxy")
end

it "to have property Auth" do
expect(resource["Properties"]["Auth"]).to eq([{"AuthScheme"=>"SECRETS", "IAMAuth"=>"REQUIRED", "SecretArn"=>{"Ref"=>"WriterSecretCredentials"}, "Description"=>"write access to the whole database"}, {"AuthScheme"=>"SECRETS", "IAMAuth"=>"REQUIRED", "SecretArn"=>{"Ref"=>"ReadonlySecretCredentials"}, "Description"=>"readonly access to the whole database"}, {"AuthScheme"=>"SECRETS", "IAMAuth"=>"REQUIRED", "SecretArn"=>{"Ref"=>"AuditSecretCredentials"}, "Description"=>"read and write access to the audit tables"}])
end

it "to have property EngineFamily" do
expect(resource["Properties"]["EngineFamily"]).to eq("MYSQL")
end

it "to have property IdleClientTimeout" do
expect(resource["Properties"]["IdleClientTimeout"]).to eq({"Ref"=>"IdleClientTimeout"})
end

it "to have property RequireTLS" do
expect(resource["Properties"]["RequireTLS"]).to eq({"Ref"=>"RequireTLS"})
end

it "to have property DBProxyName" do
expect(resource["Properties"]["DBProxyName"]).to eq({"Fn::Sub"=>"${EnvironmentName}-${ProxyName}"})
end

it "to have property RoleArn" do
expect(resource["Properties"]["RoleArn"]).to eq({"Fn::GetAtt"=>["SecretsManagerRole", "Arn"]})
end

it "to have property VpcSecurityGroupIds" do
expect(resource["Properties"]["VpcSecurityGroupIds"]).to eq([{"Ref"=>"SecurityGroup"}])
end

it "to have property VpcSubnetIds" do
expect(resource["Properties"]["VpcSubnetIds"]).to eq({"Ref"=>"SubnetIds"})
end

it "to have property Tags" do
expect(resource["Properties"]["Tags"]).to eq([{"Key"=>"Name", "Value"=>{"Fn::Sub"=>"${EnvironmentName}-rds-proxy"}}, {"Key"=>"Environment", "Value"=>{"Ref"=>"EnvironmentName"}}, {"Key"=>"EnvironmentType", "Value"=>{"Ref"=>"EnvironmentType"}}])
end

end

context "SecretsManagerRole" do
let(:resource) { template["Resources"]["SecretsManagerRole"] }

it "is of type AWS::IAM::Role" do
expect(resource["Type"]).to eq("AWS::IAM::Role")
end

it "to have property AssumeRolePolicyDocument" do
expect(resource["Properties"]["AssumeRolePolicyDocument"]).to eq({"Version"=>"2012-10-17", "Statement"=>[{"Effect"=>"Allow", "Principal"=>{"Service"=>"rds.amazonaws.com"}, "Action"=>"sts:AssumeRole"}]})
end

it "to have property Policies" do
expect(resource["Properties"]["Policies"]).to eq([{"PolicyName"=>"getsecret", "PolicyDocument"=>{"Statement"=>[{"Sid"=>"getsecret", "Action"=>["secretsmanager:GetSecretValue"], "Resource"=>[{"Ref"=>"SecretCredentials"}], "Effect"=>"Allow"}]}}])
end

end

context "ProxyTargetGroup" do
let(:resource) { template["Resources"]["ProxyTargetGroup"] }

it "is of type AWS::RDS::DBProxyTargetGroup" do
expect(resource["Type"]).to eq("AWS::RDS::DBProxyTargetGroup")
end

it "to have property ConnectionPoolConfigurationInfo" do
expect(resource["Properties"]["ConnectionPoolConfigurationInfo"]).to eq({"MaxConnectionsPercent"=>{"Ref"=>"MaxConnectionsPercent"}, "MaxIdleConnectionsPercent"=>{"Ref"=>"MaxIdleConnectionsPercent"}, "ConnectionBorrowTimeout"=>{"Ref"=>"ConnectionBorrowTimeout"}})
end

it "to have property DBProxyName" do
expect(resource["Properties"]["DBProxyName"]).to eq({"Ref"=>"RdsProxy"})
end

it "to have property DBClusterIdentifiers" do
expect(resource["Properties"]["DBClusterIdentifiers"]).to eq([{"Ref"=>"TargetDBClusterIdentifier"}])
end

it "to have property TargetGroupName" do
expect(resource["Properties"]["TargetGroupName"]).to eq("default")
end

end

context "ProxyRecord" do
let(:resource) { template["Resources"]["ProxyRecord"] }

it "is of type AWS::Route53::RecordSet" do
expect(resource["Type"]).to eq("AWS::Route53::RecordSet")
end

it "to have property HostedZoneName" do
expect(resource["Properties"]["HostedZoneName"]).to eq({"Fn::Sub"=>"${EnvironmentName}.${DnsDomain}."})
end

it "to have property Name" do
expect(resource["Properties"]["Name"]).to eq({"Fn::Sub"=>"postgres-proxy.${EnvironmentName}.${DnsDomain}."})
end

it "to have property Type" do
expect(resource["Properties"]["Type"]).to eq("CNAME")
end

it "to have property TTL" do
expect(resource["Properties"]["TTL"]).to eq("60")
end

it "to have property ResourceRecords" do
expect(resource["Properties"]["ResourceRecords"]).to eq([{"Fn::GetAtt"=>["RdsProxy", "Endpoint"]}])
end

end

end

end
5 changes: 4 additions & 1 deletion tests/disable_iam_auth.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ test_metadata:
name: disable_iam_auth
description: disable the proxy IAM authentication

iam_auth: DISABLED
users:
default:
secret_arn_parameter: SecretCredentials
iam_auth: DISABLED
20 changes: 20 additions & 0 deletions tests/multiple_users.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
test_metadata:
type: config
name: multiple_users
description: set the description for your test

users:
default:
disabled: true
writer:
secret_arn_parameter: WriterSecretCredentials
iam_auth: REQUIRED
description: write access to the whole database
readonly:
secret_arn_parameter: ReadonlySecretCredentials
iam_auth: REQUIRED
description: readonly access to the whole database
audit:
secret_arn_parameter: AuditSecretCredentials
iam_auth: REQUIRED
description: read and write access to the audit tables