From 095d8ec2562fc876c4ffffb968aeb57a1bd6efdd Mon Sep 17 00:00:00 2001 From: Panos Xynos Date: Mon, 2 Oct 2023 17:02:35 +0100 Subject: [PATCH] [185765111] Broker can accept TLS connections * Includes integration test covering selfsigned TLS --- CONFIGURATION.md | 174 +++--- Makefile | 6 +- ci/blackbox/rdsbroker_test.go | 2 +- ci/helpers/brokerapi_helper.go | 31 +- ci/integration.yml | 1 + ci/tls/config.json | 566 ++++++++++++++++++ ci/tls/tls_suite_test.go | 17 + ci/tls/tls_test.go | 139 +++++ config-sample.json | 105 +++- config/config.go | 23 +- config/tls_config.go | 45 ++ main.go | 17 +- release/jobs/rds-broker/spec | 14 + .../templates/config/rds-config.json.erb | 9 + scripts/run-broker-tls.sh | 40 ++ .../ginkgo/v2/extensions/table/deprecated.go | 3 - vendor/modules.txt | 1 - 17 files changed, 1063 insertions(+), 130 deletions(-) create mode 100644 ci/tls/config.json create mode 100644 ci/tls/tls_suite_test.go create mode 100644 ci/tls/tls_test.go create mode 100644 config/tls_config.go create mode 100755 scripts/run-broker-tls.sh delete mode 100644 vendor/github.com/onsi/ginkgo/v2/extensions/table/deprecated.go diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 19eb0994..08870c97 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -4,30 +4,42 @@ A sample configuration can be found at [config-sample.json](https://github.com/a ## General Configuration -| Option | Required | Type | Description -|:-----------------------|:--------:|:--------|:----------- -| port | N | Integer | The TCP port to listen on. Defaults to 3000 if unspecified. -| log_level | Y | String | Broker Log Level (DEBUG, INFO, ERROR, FATAL) -| username | Y | String | Broker Auth Username -| password | Y | String | Broker Auth Password -| run_housekeeping | N | Boolean | Whether to run housekeeping tasks (including master password rotation, and snapshot cleanups). This should be set to true on exactly one instance in your deployment. -| cron_schedule | Y | String | Schedule for cron jobs. A crontab-like expression with seconds precision (e.g. '0 0 * * * *' or '@hourly'), with fields: 'second minute hour dom month dow' -| keep_snapshots_for_days| Y | Integer | Number of days to keep old RDS snapshots for -| rds_config | Y | Hash | [RDS Broker configuration](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-broker-configuration) +| Option | Required | Type | Description | +| :---------------------- | :------: | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| port | N | Integer | The TCP port to listen on. Defaults to 3000 if unspecified. | +| host | N | String | The hostname or ip address that the broker accepts connections. Defaults to '0.0.0.0' if unspecified. | +| log_level | Y | String | Broker Log Level (DEBUG, INFO, ERROR, FATAL) | +| username | Y | String | Broker Auth Username | +| password | Y | String | Broker Auth Password | +| run_housekeeping | N | Boolean | Whether to run housekeeping tasks (including master password rotation, and snapshot cleanups). This should be set to true on exactly one instance in your deployment. | +| cron_schedule | Y | String | Schedule for cron jobs. A crontab-like expression with seconds precision (e.g. '0 0 * * * *' or '@hourly'), with fields: 'second minute hour dom month dow' | +| keep_snapshots_for_days | Y | Integer | Number of days to keep old RDS snapshots for | +| rds_config | Y | Hash | [RDS Broker configuration](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-broker-configuration) | +| tls | N | Hash | [RDS Broker configuration](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-broker-tls-configuration) | ## RDS Broker Configuration -| Option | Required | Type | Description -|:-------------------------------|:--------:|:------- |:----------- -| region | Y | String | RDS Region -| db_prefix | Y | String | Prefix to add to RDS DB Identifiers -| allow_user_provision_parameters| N | Boolean | Allow users to send arbitrary parameters on provision calls (defaults to `false`) -| allow_user_update_parameters | N | Boolean | Allow users to send arbitrary parameters on update calls (defaults to `false`) -| allow_user_bind_parameters | N | Boolean | Allow users to send arbitrary parameters on bind calls (defaults to `false`) -| catalog | Y | Hash | [RDS Broker catalog](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-broker-catalog) -| master_password_seed | Y | String | Seed to generate DB instances master passwords -| aws_tag_cache_seconds | N | Integer | Cache expiry time of AWS Tags cache (in seconds) -| broker_name | Y | String | RDS broker name used to tag instances for identification +| Option | Required | Type | Description | +| :------------------------------ | :------: | :------ | :---------------------------------------------------------------------------------------------------------------- | +| region | Y | String | RDS Region | +| db_prefix | Y | String | Prefix to add to RDS DB Identifiers | +| allow_user_provision_parameters | N | Boolean | Allow users to send arbitrary parameters on provision calls (defaults to `false`) | +| allow_user_update_parameters | N | Boolean | Allow users to send arbitrary parameters on update calls (defaults to `false`) | +| allow_user_bind_parameters | N | Boolean | Allow users to send arbitrary parameters on bind calls (defaults to `false`) | +| catalog | Y | Hash | [RDS Broker catalog](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-broker-catalog) | +| master_password_seed | Y | String | Seed to generate DB instances master passwords | +| aws_tag_cache_seconds | N | Integer | Cache expiry time of AWS Tags cache (in seconds) | +| broker_name | Y | String | RDS broker name used to tag instances for identification | + +## RDS Broker TLS Configuration + +> If the configuration is provided all fields are required. +> To try how this works `scripts/run-broker-tls.sh` will run RDS broker locally with self signed certificate. + +| Option | Required | Type | Description | +| :---------- | :------: | :----- | :----------------------------------------- | +| certificate | Y | String | The certificate to use for TLS connection. | +| private_key | Y | String | The private key to use for TLS connection. | ### Note When the seed is changed and the broker restarted, the instances master passwords will be updated. @@ -38,74 +50,74 @@ Please refer to the [Catalog Documentation](https://docs.cloudfoundry.org/servic ### Catalog -| Option | Required | Type | Description -|:---------|:--------:|:--------- |:----------- -| services | N | []Service | A list of [Services](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#service) +| Option | Required | Type | Description | +| :------- | :------: | :-------- | :----------------------------------------------------------------------------------------------------- | +| services | N | []Service | A list of [Services](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#service) | ### Service -| Option | Required | Type | Description -|:------------------------------|:--------:|:------------- |:----------- -| id | Y | String | An identifier used to correlate this service in future requests to the catalog -| name | Y | String | The CLI-friendly name of the service that will appear in the catalog. All lowercase, no spaces -| description | Y | String | A short description of the service that will appear in the catalog -| tags | N | []String | A list of service tags -| metadata.displayName | N | String | The name of the service to be displayed in graphical clients -| metadata.imageUrl | N | String | The URL to an image -| metadata.longDescription | N | String | Long description -| metadata.providerDisplayName | N | String | The name of the upstream entity providing the actual service -| metadata.documentationUrl | N | String | Link to documentation page for service -| metadata.supportUrl | N | String | Link to support for the service -| requires | N | []String | A list of permissions that the user would have to give the service, if they provision it (only `syslog_drain` is supported) -| plan_updateable | N | Boolean | Whether the service supports upgrade/downgrade for some plans -| plans | N | []ServicePlan | A list of [Plans](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#service-plan) for this service -| dashboard_client.id | N | String | The id of the Oauth2 client that the service intends to use -| dashboard_client.secret | N | String | A secret for the dashboard client -| dashboard_client.redirect_uri | N | String | A domain for the service dashboard that will be whitelisted by the UAA to enable SSO +| Option | Required | Type | Description | +| :---------------------------- | :------: | :------------ | :-------------------------------------------------------------------------------------------------------------------------- | +| id | Y | String | An identifier used to correlate this service in future requests to the catalog | +| name | Y | String | The CLI-friendly name of the service that will appear in the catalog. All lowercase, no spaces | +| description | Y | String | A short description of the service that will appear in the catalog | +| tags | N | []String | A list of service tags | +| metadata.displayName | N | String | The name of the service to be displayed in graphical clients | +| metadata.imageUrl | N | String | The URL to an image | +| metadata.longDescription | N | String | Long description | +| metadata.providerDisplayName | N | String | The name of the upstream entity providing the actual service | +| metadata.documentationUrl | N | String | Link to documentation page for service | +| metadata.supportUrl | N | String | Link to support for the service | +| requires | N | []String | A list of permissions that the user would have to give the service, if they provision it (only `syslog_drain` is supported) | +| plan_updateable | N | Boolean | Whether the service supports upgrade/downgrade for some plans | +| plans | N | []ServicePlan | A list of [Plans](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#service-plan) for this service | +| dashboard_client.id | N | String | The id of the Oauth2 client that the service intends to use | +| dashboard_client.secret | N | String | A secret for the dashboard client | +| dashboard_client.redirect_uri | N | String | A domain for the service dashboard that will be whitelisted by the UAA to enable SSO | ### Service Plan -| Option | Required | Type | Description -|:---------------------|:--------:|:------------- |:----------- -| id | Y | String | An identifier used to correlate this plan in future requests to the catalog -| name | Y | String | The CLI-friendly name of the plan that will appear in the catalog. All lowercase, no spaces -| description | Y | String | A short description of the plan that will appear in the catalog -| metadata.bullets | N | []String | Features of this plan, to be displayed in a bulleted-list -| metadata.costs | N | Cost Object | An array-of-objects that describes the costs of a service, in what currency, and the unit of measure -| metadata.displayName | N | String | Name of the plan to be display in graphical clients -| free | N | Boolean | This field allows the plan to be limited by the non_basic_services_allowed field in a Cloud Foundry Quota -| rds_properties | Y | RDSProperties | [RDS Properties](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-properties) +| Option | Required | Type | Description | +| :------------------- | :------: | :------------ | :-------------------------------------------------------------------------------------------------------- | +| id | Y | String | An identifier used to correlate this plan in future requests to the catalog | +| name | Y | String | The CLI-friendly name of the plan that will appear in the catalog. All lowercase, no spaces | +| description | Y | String | A short description of the plan that will appear in the catalog | +| metadata.bullets | N | []String | Features of this plan, to be displayed in a bulleted-list | +| metadata.costs | N | Cost Object | An array-of-objects that describes the costs of a service, in what currency, and the unit of measure | +| metadata.displayName | N | String | Name of the plan to be display in graphical clients | +| free | N | Boolean | This field allows the plan to be limited by the non_basic_services_allowed field in a Cloud Foundry Quota | +| rds_properties | Y | RDSProperties | [RDS Properties](https://github.com/alphagov/paas-rds-broker/blob/master/CONFIGURATION.md#rds-properties) | ## RDS Properties Please refer to the [Amazon Relational Database Service Documentation](https://aws.amazon.com/documentation/rds/) for more details about these properties. -| Option | Required | Type | Description -|:--------------------------------|:--------:|:--------- |:----------- -| allocated_storage | Y | Integer | The amount of storage (in gigabytes) to be initially allocated for the database instances (between `5` and `6144`) -| auto_minor_version_upgrade | N | Boolean | Enable or disable automatic upgrades to new minor versions as they are released (defaults to `false`) -| availability_zone | N | String | The Availability Zone that database instances will be created in -| backup_retention_period | N | Integer | The number of days that Amazon RDS should retain automatic backups of DB instances (between `0` and `35`) -| character_set_name | N | String | For supported engines, indicates that DB instances should be associated with the specified CharacterSet -| copy_tags_to_snapshot | N | Boolean | Enable or disable copying all tags from DB instances to snapshots -| db_instance_class | Y | String | The name of the DB Instance Class -| db_security_groups | N | []String | The security group(s) names that have rules authorizing connections from applications that need to access the data stored in the DB instance -| db_subnet_group_name | N | String | The DB subnet group name that defines which subnets and IP ranges the DB instance can use in the VPC -| engine | Y | String | The name of the Database Engine (only `mariadb`, `mysql` and `postgres` are supported) -| engine_version | Y | String | The version number of the Database Engine -| engine_family | Y | String | The family name for the engine and version, as reported by RDS -| iops | N | Integer | The amount of Provisioned IOPS to be initially allocated for DB instances when using `io1` storage type -| kms_key_id | N | String | The KMS key identifier for encrypted DB instances -| license_model | N | String | License model information for DB instances (`license-included`, `bring-your-own-license`, `general-public-license`) -| multi_az | N | Boolean | Enable or disable Multi-AZ deployment for high availability DB Instances -| option_group_name | N | String | The DB option group name that enables any optional functionality you want the DB instances to support -| port | N | Integer | The TCP/IP port DB instances will use for application connections -| preferred_backup_window | N | String | The daily time range during which automated backups are created if automated backups are enabled -| preferred_maintenance_window | N | String | The weekly time range during which system maintenance can occur -| publicly_accessible | N | Boolean | Specify if DB instances will be publicly accessible -| skip_final_snapshot | N | Boolean | Determines whether a final DB snapshot is created before the DB instances are deleted -| storage_encrypted | N | Boolean | Specifies whether DB instances are encrypted -| storage_type | N | String | The storage type to be associated with DB instances (`standard`, `gp2`, `io1`) -| vpc_security_group_ids | N | []String | VPC security group(s) IDs that have rules authorizing connections from applications that need to access the data stored in DB instances -| allowed_extensions | Y | []String | The set of Postgres extensions which can be enabled -| default_extensions | Y | []String | The set of Postgres extensions which are enabled by default. Each of these must also be in the `allowed_extensions` list. +| Option | Required | Type | Description | +| :--------------------------- | :------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------- | +| allocated_storage | Y | Integer | The amount of storage (in gigabytes) to be initially allocated for the database instances (between `5` and `6144`) | +| auto_minor_version_upgrade | N | Boolean | Enable or disable automatic upgrades to new minor versions as they are released (defaults to `false`) | +| availability_zone | N | String | The Availability Zone that database instances will be created in | +| backup_retention_period | N | Integer | The number of days that Amazon RDS should retain automatic backups of DB instances (between `0` and `35`) | +| character_set_name | N | String | For supported engines, indicates that DB instances should be associated with the specified CharacterSet | +| copy_tags_to_snapshot | N | Boolean | Enable or disable copying all tags from DB instances to snapshots | +| db_instance_class | Y | String | The name of the DB Instance Class | +| db_security_groups | N | []String | The security group(s) names that have rules authorizing connections from applications that need to access the data stored in the DB instance | +| db_subnet_group_name | N | String | The DB subnet group name that defines which subnets and IP ranges the DB instance can use in the VPC | +| engine | Y | String | The name of the Database Engine (only `mariadb`, `mysql` and `postgres` are supported) | +| engine_version | Y | String | The version number of the Database Engine | +| engine_family | Y | String | The family name for the engine and version, as reported by RDS | +| iops | N | Integer | The amount of Provisioned IOPS to be initially allocated for DB instances when using `io1` storage type | +| kms_key_id | N | String | The KMS key identifier for encrypted DB instances | +| license_model | N | String | License model information for DB instances (`license-included`, `bring-your-own-license`, `general-public-license`) | +| multi_az | N | Boolean | Enable or disable Multi-AZ deployment for high availability DB Instances | +| option_group_name | N | String | The DB option group name that enables any optional functionality you want the DB instances to support | +| port | N | Integer | The TCP/IP port DB instances will use for application connections | +| preferred_backup_window | N | String | The daily time range during which automated backups are created if automated backups are enabled | +| preferred_maintenance_window | N | String | The weekly time range during which system maintenance can occur | +| publicly_accessible | N | Boolean | Specify if DB instances will be publicly accessible | +| skip_final_snapshot | N | Boolean | Determines whether a final DB snapshot is created before the DB instances are deleted | +| storage_encrypted | N | Boolean | Specifies whether DB instances are encrypted | +| storage_type | N | String | The storage type to be associated with DB instances (`standard`, `gp2`, `io1`) | +| vpc_security_group_ids | N | []String | VPC security group(s) IDs that have rules authorizing connections from applications that need to access the data stored in DB instances | +| allowed_extensions | Y | []String | The set of Postgres extensions which can be enabled | +| default_extensions | Y | []String | The set of Postgres extensions which are enabled by default. Each of these must also be in the `allowed_extensions` list. | diff --git a/Makefile b/Makefile index aa2cbf7d..ba0d11fd 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ MYSQL_PASSWORD=toor integration: go run github.com/onsi/ginkgo/v2/ginkgo --timeout=5h --nodes=4 -r ci/blackbox --slowSpecThreshold=1800 -stream -failFast +.PHONY: tls_integration +tls_integration: + go run github.com/onsi/ginkgo/v2/ginkgo --timeout=5h -r ci/tls --slowSpecThreshold=1800 -stream -failFast + .PHONY: unit unit: test_unit test_all_sql @@ -14,7 +18,7 @@ test_unit: .PHONY: test_all_sql test_all_sql: test_postgres test_mysql .PHONY: test_postgres -test_postgres: run_postgres_sql_tests start_postgres_10 run_postgres_sql_tests stop_postgres_10 start_postgres_11 run_postgres_sql_tests stop_postgres_11 start_postgres_12 run_postgres_sql_tests stop_postgres_12 start_postgres_13 run_postgres_sql_tests stop_postgres_13 +test_postgres: start_postgres_10 run_postgres_sql_tests stop_postgres_10 start_postgres_11 run_postgres_sql_tests stop_postgres_11 start_postgres_12 run_postgres_sql_tests stop_postgres_12 start_postgres_13 run_postgres_sql_tests stop_postgres_13 .PHONY: test_mysql test_mysql: start_mysql_80 run_mysql_sql_tests stop_mysql_80 start_mysql_57 run_mysql_sql_tests stop_mysql_57 diff --git a/ci/blackbox/rdsbroker_test.go b/ci/blackbox/rdsbroker_test.go index db2898b6..b05dd03a 100644 --- a/ci/blackbox/rdsbroker_test.go +++ b/ci/blackbox/rdsbroker_test.go @@ -1163,7 +1163,7 @@ func startNewBroker(rdsBrokerConfig *config.Config, brokerName string) (*gexec.S // Wait for it to be listening Eventually(rdsBrokerSession, 10*time.Second).Should(And( gbytes.Say("rds-broker.start"), - gbytes.Say(fmt.Sprintf(`{"port":%d}`, rdsBrokerPort)), + gbytes.Say(fmt.Sprintf(`{"address":"0.0.0.0:%d","host":"0.0.0.0","port":%d,"tls":false}`, rdsBrokerPort, rdsBrokerPort)), )) rdsBrokerUrl := fmt.Sprintf("http://localhost:%d", rdsBrokerPort) diff --git a/ci/helpers/brokerapi_helper.go b/ci/helpers/brokerapi_helper.go index 4b8c3fda..383ce2c9 100644 --- a/ci/helpers/brokerapi_helper.go +++ b/ci/helpers/brokerapi_helper.go @@ -2,6 +2,7 @@ package helpers import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -47,23 +48,37 @@ func BodyBytes(resp *http.Response) ([]byte, error) { } type BrokerAPIClient struct { - Url string - Username string - Password string - AcceptsIncomplete bool + Url string + Username string + Password string + AcceptsIncomplete bool + AcceptsSelfSignedCerts bool } func NewBrokerAPIClient(Url string, Username string, Password string) *BrokerAPIClient { return &BrokerAPIClient{ - Url: Url, - Username: Username, - Password: Password, + Url: Url, + Username: Username, + Password: Password, + AcceptsSelfSignedCerts: true, } } func (b *BrokerAPIClient) doRequest(action string, path string, body io.Reader, params ...uriParam) (*http.Response, error) { - client := &http.Client{} + var client *http.Client + if b.AcceptsSelfSignedCerts { + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + } else { + client = &http.Client{} + + } req, err := http.NewRequest(action, b.Url+path, body) if err != nil { diff --git a/ci/integration.yml b/ci/integration.yml index 43f11b87..82df6d04 100644 --- a/ci/integration.yml +++ b/ci/integration.yml @@ -15,4 +15,5 @@ run: - -c - | cd src/github.com/alphagov/paas-rds-broker + make tls_integration make integration diff --git a/ci/tls/config.json b/ci/tls/config.json new file mode 100644 index 00000000..3537a7c2 --- /dev/null +++ b/ci/tls/config.json @@ -0,0 +1,566 @@ +{ + "log_level": "DEBUG", + "username": "username", + "password": "password", + "cron_schedule": "*/5 * * * *", + "keep_snapshots_for_days": 7, + "tls": { + "certificate": "__from_maketarget__", + "private_key": "__from_maketarget__" + }, + "rds_config": { + "region": "us-east-1", + "db_prefix": "cf", + "allow_user_provision_parameters": true, + "allow_user_update_parameters": true, + "allow_user_bind_parameters": true, + "master_password_seed": "jK5Q45Gx2q4I", + "broker_name": "GDS-prod-1", + "catalog": { + "services": [ + { + "id": "ce71b484-d542-40f7-9dd4-5526e38c81ba", + "name": "rdsmysql", + "description": "RDS MySQL service", + "bindable": true, + "tags": [ + "mysql", + "relational" + ], + "metadata": { + "displayName": "RDS MySQL", + "imageUrl": "data:image/png;base64,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", + "longDescription": "A RDS MySQL service", + "providerDisplayName": "Amazon Web Services", + "documentationUrl": "https://aws.amazon.com/documentation/rds/", + "supportUrl": "https://forums.aws.amazon.com/forum.jspa?forumID=60" + }, + "plan_updateable": true, + "plans": [ + { + "id": "326b78b0-a8ab-4cc0-8657-79c9c0ac8126", + "name": "5.5-medium", + "description": "RDS MySQL 5.5 (db.m3.medium, 10Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.180 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated MySQL 5.5 server", + "MySQL 5.5", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.medium", + "engine": "MySQL", + "engine_version": "5.5.42", + "engine_family": "mysql5.5", + "allocated_storage": 10, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [], + "default_extensions": [] + } + }, + { + "id": "729d81e7-29a0-4709-bdf2-3317a1468291", + "name": "5.5-large", + "description": "RDS MySQL 5.5 (db.m3.large, 50Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.370 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated MySQL 5.5 server", + "MySQL 5.5", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.large", + "engine": "MySQL", + "engine_version": "5.5.42", + "engine_family": "mysql5.5", + "allocated_storage": 50, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [], + "default_extensions": [] + } + }, + { + "id": "499e9ff5-28a7-43eb-bf93-7bc69b61712b", + "name": "5.5-xlarge", + "description": "RDS MySQL 5.5 (db.m3.xlarge, 100Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.740 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated MySQL 5.5 server", + "MySQL 5.5", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.xlarge", + "engine": "MySQL", + "engine_version": "5.5.42", + "engine_family": "mysql5.5", + "allocated_storage": 100, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [], + "default_extensions": [] + } + }, + { + "id": "5b8282cf-a669-4ffc-b426-c169a7bbfc71", + "name": "5.6-medium", + "description": "RDS MySQL 5.6 (db.m3.medium, 10Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.180 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated MySQL 5.6 server", + "MySQL 5.6", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.medium", + "engine": "MySQL", + "engine_version": "5.6.23", + "engine_family": "mysql5.6", + "allocated_storage": 10, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [], + "default_extensions": [] + } + }, + { + "id": "b4943c34-fd33-47a6-8f0b-eb4f462fd746", + "name": "5.6-large", + "description": "RDS MySQL 5.6 (db.m3.large, 50Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.370 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated MySQL 5.6 server", + "MySQL 5.6", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.large", + "engine": "MySQL", + "engine_version": "5.6.23", + "engine_family": "mysql5.6", + "allocated_storage": 50, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [], + "default_extensions": [] + } + }, + { + "id": "7e47cd05-625e-415d-bafd-09fbb0eb9ed8", + "name": "5.6-xlarge", + "description": "RDS MySQL 5.6 (db.m3.xlarge, 100Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.740 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated MySQL 5.6 server", + "MySQL 5.6", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.xlarge", + "engine": "MySQL", + "engine_version": "5.6.23", + "engine_family": "mysql5.6", + "allocated_storage": 100, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [], + "default_extensions": [] + } + } + ] + }, + { + "id": "a2c9adda-6511-462c-9934-b3fd8236e9f0", + "name": "rdspostgres", + "description": "RDS PostgreSQL service", + "bindable": true, + "tags": [ + "postgres", + "relational" + ], + "metadata": { + "displayName": "RDS PostgreSQL", + "imageUrl": "data:image/png;base64,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", + "longDescription": "RDS PostgreSQL service", + "providerDisplayName": "Amazon Web Services", + "documentationUrl": "https://aws.amazon.com/documentation/rds/", + "supportUrl": "https://forums.aws.amazon.com/forum.jspa?forumID=60" + }, + "plan_updateable": true, + "plans": [ + { + "id": "d42fc3cc-1341-4aa3-866e-01bc5243dc3e", + "name": "9.6-medium", + "description": "RDS PostgreSQL 9.6 (db.m3.medium, 10Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.180 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated PostgreSQL 9.6 server", + "PostgreSQL 9.6", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.medium", + "engine": "postgres", + "engine_version": "9.6", + "engine_family": "postgres9.6", + "allocated_storage": 10, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] + } + }, + { + "id": "80768f31-5c2c-40e8-8135-59fe3d710dc3", + "name": "9.6-large", + "description": "RDS PostgreSQL 9.6 (db.m3.large, 50Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.370 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated PostgreSQL 9.6 server", + "PostgreSQL 9.6", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.large", + "engine": "postgres", + "engine_version": "9.6", + "engine_family": "postgres9.6", + "allocated_storage": 50, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] + } + }, + { + "id": "347dfd90-4f0c-42ed-a92c-1d8096d373d1", + "name": "9.6-xlarge", + "description": "RDS PostgreSQL 9.6 (db.m3.xlarge, 100Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.740 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated PostgreSQL 9.6 server", + "PostgreSQL 9.6", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.xlarge", + "engine": "postgres", + "engine_version": "9.6", + "engine_family": "postgres9.6", + "allocated_storage": 100, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] + } + }, + { + "id": "d03b544e-3be5-4aca-bb3b-11544247f313", + "name": "9.5-medium", + "description": "RDS PostgreSQL 9.5 (db.m3.medium, 10Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.180 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated PostgreSQL 9.5 server", + "PostgreSQL 9.5", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.medium", + "engine": "postgres", + "engine_version": "9.5", + "engine_family": "postgres9.5", + "allocated_storage": 10, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] + } + }, + { + "id": "65cfcd68-625d-4635-a828-4ccef50af986", + "name": "9.5-large", + "description": "RDS PostgreSQL 9.5 (db.m3.large, 50Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.370 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated PostgreSQL 9.5 server", + "PostgreSQL 9.5", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.large", + "engine": "postgres", + "engine_version": "9.5", + "engine_family": "postgres9.5", + "allocated_storage": 50, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] + } + }, + { + "id": "8f8d8ed7-ddd3-4b1f-befa-976cf7fc4116", + "name": "9.5-xlarge", + "description": "RDS PostgreSQL 9.5 (db.m3.xlarge, 100Gb)", + "metadata": { + "costs": [ + { + "amount": { + "usd": 0.740 + }, + "unit": "HOUR" + } + ], + "bullets": [ + "Dedicated PostgreSQL 9.5 server", + "PostgreSQL 9.5", + "AWS RDS" + ] + }, + "free": false, + "rds_properties": { + "db_instance_class": "db.m3.xlarge", + "engine": "postgres", + "engine_version": "9.5", + "engine_family": "postgres9.5", + "allocated_storage": 100, + "auto_minor_version_upgrade": true, + "multi_az": true, + "publicly_accessible": true, + "copy_tags_to_snapshot": true, + "skip_final_snapshot": false, + "db_security_groups": [ + "default" + ], + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] + } + } + ] + } + ], + "exclude_engines": [ + { + "engine": "postgres", + "engine_version": "^9\\.3\\." + }, + { + "engine": "postgres", + "engine_version": "^9\\.4\\.[0-8]$" + }, + { + "engine": "postgres", + "engine_version": "^9\\.5\\.[0-4]$" + } + ] + } + } +} diff --git a/ci/tls/tls_suite_test.go b/ci/tls/tls_suite_test.go new file mode 100644 index 00000000..64a0545c --- /dev/null +++ b/ci/tls/tls_suite_test.go @@ -0,0 +1,17 @@ +package tls_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type SuiteData struct { + RdsBrokerPath string +} + +func TestTls(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Tls Suite") +} diff --git a/ci/tls/tls_test.go b/ci/tls/tls_test.go new file mode 100644 index 00000000..684133b7 --- /dev/null +++ b/ci/tls/tls_test.go @@ -0,0 +1,139 @@ +package tls_test + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "os" + "os/exec" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + uuid "github.com/satori/go.uuid" + + . "github.com/alphagov/paas-rds-broker/ci/helpers" + "github.com/alphagov/paas-rds-broker/config" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/phayes/freeport" +) + +var ( + rdsBrokerSession *gexec.Session + brokerAPIClient *BrokerAPIClient + brokerName string + rdsBrokerPath string +) + +var _ = Describe("TLS Support", func() { + BeforeEach(func() { + var err error + + // Give a different Broker Name in each execution, to avoid conflicts + brokerName = fmt.Sprintf( + "%s-%s", + "rdsbroker-integration-test", + uuid.NewV4().String(), + ) + rdsBrokerPath, err = gexec.Build("github.com/alphagov/paas-rds-broker") + Expect(err).ShouldNot(HaveOccurred()) + + jsonConfigFilename := "config.json" + jsonFilePath, err := filepath.Abs(jsonConfigFilename) + Expect(err).ShouldNot(HaveOccurred()) + + jsonData, err := os.ReadFile(jsonFilePath) + Expect(err).ShouldNot(HaveOccurred()) + + var testConfig config.Config + err = json.Unmarshal(jsonData, &testConfig) + Expect(err).ShouldNot(HaveOccurred()) + + rdsBrokerSession, brokerAPIClient, _ = startNewBroker(&testConfig, brokerName) + }) + + AfterEach(func() { + rdsBrokerSession.Kill() + }) + + Describe("Services", func() { + It("returns the proper CatalogResponse", func() { + _, err := brokerAPIClient.GetCatalog() + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) + +func startNewBroker(rdsBrokerConfig *config.Config, brokerName string) (*gexec.Session, *BrokerAPIClient, *RDSClient) { + configFile, err := os.CreateTemp("", "rds-broker") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(configFile.Name()) + + newRDSBrokerConfig := *rdsBrokerConfig + // start the broker in a random port + rdsBrokerPort := freeport.GetPort() + newRDSBrokerConfig.Port = rdsBrokerPort + + newRDSBrokerConfig.RDSConfig.BrokerName = "tls_broker" + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + Expect(err).NotTo(HaveOccurred()) + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + privBytes := x509.MarshalPKCS1PrivateKey(priv) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) + + newRDSBrokerConfig.TLS = &config.TLSConfig{ + Certificate: string(certPEM), + PrivateKey: string(keyPEM), + } + + configJSON, err := json.Marshal(&newRDSBrokerConfig) + Expect(err).ToNot(HaveOccurred()) + Expect(os.WriteFile(configFile.Name(), configJSON, 0644)).To(Succeed()) + Expect(configFile.Close()).To(Succeed()) + + command := exec.Command(rdsBrokerPath, + fmt.Sprintf("-config=%s", configFile.Name()), + ) + fmt.Printf("\n\n\n----------%s\n%s-----------\n\n", rdsBrokerPath, configFile.Name()) + rdsBrokerSession, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + // Wait for it to be listening + Eventually(rdsBrokerSession, 10*time.Second).Should(And( + gbytes.Say("rds-broker.start"), + gbytes.Say(fmt.Sprintf(`{"address":"0.0.0.0:%d","host":"0.0.0.0","port":%d,"tls":true}`, rdsBrokerPort, rdsBrokerPort)), + )) + + rdsBrokerUrl := fmt.Sprintf("https://localhost:%d", rdsBrokerPort) + + brokerAPIClient := NewBrokerAPIClient(rdsBrokerUrl, rdsBrokerConfig.Username, rdsBrokerConfig.Password) + rdsClient, err := NewRDSClient(rdsBrokerConfig.RDSConfig.Region, rdsBrokerConfig.RDSConfig.DBPrefix) + + Expect(err).ToNot(HaveOccurred()) + + return rdsBrokerSession, brokerAPIClient, rdsClient +} diff --git a/config-sample.json b/config-sample.json index fd22d513..3537a7c2 100644 --- a/config-sample.json +++ b/config-sample.json @@ -4,6 +4,10 @@ "password": "password", "cron_schedule": "*/5 * * * *", "keep_snapshots_for_days": 7, + "tls": { + "certificate": "__from_maketarget__", + "private_key": "__from_maketarget__" + }, "rds_config": { "region": "us-east-1", "db_prefix": "cf", @@ -62,7 +66,7 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" @@ -100,7 +104,7 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" @@ -138,7 +142,7 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" @@ -176,7 +180,7 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" @@ -214,7 +218,7 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" @@ -252,7 +256,7 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" @@ -311,16 +315,21 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" ], - "allowed_extensions": ["postgis", "pg_stat_statements"], - "default_extensions": ["postgis"] + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] } }, - { + { "id": "80768f31-5c2c-40e8-8135-59fe3d710dc3", "name": "9.6-large", "description": "RDS PostgreSQL 9.6 (db.m3.large, 50Gb)", @@ -349,16 +358,21 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" ], - "allowed_extensions": ["postgis", "pg_stat_statements"], - "default_extensions": ["postgis"] + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] } }, - { + { "id": "347dfd90-4f0c-42ed-a92c-1d8096d373d1", "name": "9.6-xlarge", "description": "RDS PostgreSQL 9.6 (db.m3.xlarge, 100Gb)", @@ -387,13 +401,18 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" ], - "allowed_extensions": ["postgis", "pg_stat_statements"], - "default_extensions": ["postgis"] + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] } }, { @@ -425,16 +444,21 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" ], - "allowed_extensions": ["postgis", "pg_stat_statements"], - "default_extensions": ["postgis"] + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] } }, - { + { "id": "65cfcd68-625d-4635-a828-4ccef50af986", "name": "9.5-large", "description": "RDS PostgreSQL 9.5 (db.m3.large, 50Gb)", @@ -463,15 +487,20 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" ], - "allowed_extensions": ["postgis", "pg_stat_statements"], - "default_extensions": ["postgis"] + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] } - }, + }, { "id": "8f8d8ed7-ddd3-4b1f-befa-976cf7fc4116", "name": "9.5-xlarge", @@ -501,22 +530,36 @@ "auto_minor_version_upgrade": true, "multi_az": true, "publicly_accessible": true, - "copy_tags_to_snapshot":true, + "copy_tags_to_snapshot": true, "skip_final_snapshot": false, "db_security_groups": [ "default" ], - "allowed_extensions": ["postgis", "pg_stat_statements"], - "default_extensions": ["postgis"] + "allowed_extensions": [ + "postgis", + "pg_stat_statements" + ], + "default_extensions": [ + "postgis" + ] } } ] } ], "exclude_engines": [ - {"engine": "postgres", "engine_version": "^9\\.3\\."}, - {"engine": "postgres", "engine_version": "^9\\.4\\.[0-8]$"}, - {"engine": "postgres", "engine_version": "^9\\.5\\.[0-4]$"} + { + "engine": "postgres", + "engine_version": "^9\\.3\\." + }, + { + "engine": "postgres", + "engine_version": "^9\\.4\\.[0-8]$" + }, + { + "engine": "postgres", + "engine_version": "^9\\.5\\.[0-4]$" + } ] } } diff --git a/config/config.go b/config/config.go index 1ac2eb68..c8b30668 100644 --- a/config/config.go +++ b/config/config.go @@ -9,15 +9,22 @@ import ( "github.com/alphagov/paas-rds-broker/rdsbroker" ) +const ( + DefaultPort = 3000 + DefaultHost = "0.0.0.0" +) + type Config struct { Port int `json:"port"` LogLevel string `json:"log_level"` Username string `json:"username"` Password string `json:"password"` + Host string `json:"host"` RunHousekeeping bool `json:"run_housekeeping"` KeepSnapshotsForDays int `json:"keep_snapshots_for_days"` CronSchedule string `json:"cron_schedule"` RDSConfig *rdsbroker.Config `json:"rds_config"` + TLS *TLSConfig `json:"tls"` } func LoadConfig(configFile string) (config *Config, err error) { @@ -46,11 +53,18 @@ func LoadConfig(configFile string) (config *Config, err error) { func (c *Config) FillDefaults() { if c.Port == 0 { - c.Port = 3000 + c.Port = DefaultPort + } + if c.Host == "" { + c.Host = DefaultHost } c.RDSConfig.FillDefaults() } +func (c Config) TLSEnabled() bool { + return c.TLS != nil +} + func (c Config) Validate() error { if c.LogLevel == "" { return errors.New("Must provide a non-empty LogLevel") @@ -76,5 +90,12 @@ func (c Config) Validate() error { return fmt.Errorf("Validating RDS configuration: %s", err) } + if c.TLS != nil { + tlsValidation := c.TLS.validate() + if tlsValidation != nil { + return tlsValidation + } + } + return nil } diff --git a/config/tls_config.go b/config/tls_config.go new file mode 100644 index 00000000..b3380535 --- /dev/null +++ b/config/tls_config.go @@ -0,0 +1,45 @@ +package config + +import ( + "crypto/tls" + "fmt" +) + +type TLSConfig struct { + Certificate string `json:"certificate"` + PrivateKey string `json:"private_key"` +} + +// GenerateTLSConfig produces a tls.Config structure out of TLSConfig. +// Aiming to be used while configuring a TLS client or server. +func (t *TLSConfig) GenerateTLSConfig() (*tls.Config, error) { + certificate, err := tls.X509KeyPair([]byte(t.Certificate), []byte(t.PrivateKey)) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{certificate}, + + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + }, nil +} + +func (t *TLSConfig) validate() error { + if t.Certificate == "" { + return fmt.Errorf("Config error: TLS certificate required") + } + if t.PrivateKey == "" { + return fmt.Errorf("Config error: TLS private key required") + } + return nil +} diff --git a/main.go b/main.go index 94a3c8a9..02b5c445 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "flag" "fmt" "log" @@ -97,15 +98,25 @@ func startHTTPServer( ) error { server := buildHTTPHandler(serviceBroker, logger, cfg) + listenAddress := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) // We don't use http.ListenAndServe here so that the "start" log message is // logged after the socket is listening. This log message is used by the // tests to wait until the broker is ready. - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) + listener, err := net.Listen("tcp", listenAddress) if err != nil { - return fmt.Errorf("failed to listen on port %d: %s", cfg.Port, err) + return fmt.Errorf("failed to listen on address %s: %s", listenAddress, err) + } + if cfg.TLSEnabled() { + tlsConfig, err := cfg.TLS.GenerateTLSConfig() + if err != nil { + log.Fatalf("Error configuring TLS: %s", err) + } + listener = tls.NewListener(listener, tlsConfig) + logger.Info("start", lager.Data{"port": cfg.Port, "tls": true, "host": cfg.Host, "address": listenAddress}) + } else { + logger.Info("start", lager.Data{"port": cfg.Port, "tls": false, "host": cfg.Host, "address": listenAddress}) } - logger.Info("start", lager.Data{"port": cfg.Port}) return http.Serve(listener, server) } diff --git a/release/jobs/rds-broker/spec b/release/jobs/rds-broker/spec index b7337e24..56ba8dae 100644 --- a/release/jobs/rds-broker/spec +++ b/release/jobs/rds-broker/spec @@ -17,6 +17,9 @@ properties: rds-broker.port: description: "Broker Listen Port" default: 80 + rds-broker.host: + description: "Broker Listen hostname or IP" + example: "0.0.0.0" rds-broker.username: description: "Broker Auth Username" default: "rds-broker" @@ -60,3 +63,14 @@ properties: rds-broker.keep_snapshots_for_days: description: "Number of days to keep old RDS snapshots for" default: 35 + rds-broker.tls: + description: "Certificate and private key for TLS listener" + example: | + certificate: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + private_key: | + -----BEGIN EXAMPLE RSA PRIVATE KEY----- + ... + -----END EXAMPLE RSA PRIVATE KEY----- diff --git a/release/jobs/rds-broker/templates/config/rds-config.json.erb b/release/jobs/rds-broker/templates/config/rds-config.json.erb index 019ffda4..2632640c 100644 --- a/release/jobs/rds-broker/templates/config/rds-config.json.erb +++ b/release/jobs/rds-broker/templates/config/rds-config.json.erb @@ -1,5 +1,8 @@ { "port": <%= p('rds-broker.port') %>, +<% if_p('rds-broker.host') do |host| -%> + "host": "<%= host %>", +<% end -%> "log_level": "<%= p('rds-broker.log_level') %>", "username": "<%= p('rds-broker.username') %>", "password": "<%= p('rds-broker.password') %>", @@ -7,6 +10,12 @@ "cron_schedule": "<%= p('rds-broker.cron_schedule') %>", "keep_snapshots_for_days": <%= p('rds-broker.keep_snapshots_for_days') %>, "state_encryption_key": "<%= p('rds-broker.state_encryption_key') %>", +<% if_p('rds-broker.tls') do |tls| -%> + "tls": { + "certificate": "<%= tls.fetch("certificate").gsub(/\n/, '\\n') %>", + "private_key": "<%= tls.fetch("private_key").gsub(/\n/, '\\n') %>" + }, +<% end -%> "rds_config": { "region": "<%= p('rds-broker.aws_region') %>", "db_prefix": "<%= p('rds-broker.db_prefix') %>", diff --git a/scripts/run-broker-tls.sh b/scripts/run-broker-tls.sh new file mode 100755 index 00000000..190d63c9 --- /dev/null +++ b/scripts/run-broker-tls.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) + +_tmp_dir="$(mktemp -d)" +trap 'rm -rf "${_tmp_dir}"' EXIT + +pushd "${_tmp_dir}" > /dev/null + echo "Generating TLS Certificates" + # Generate the CA Key and Certificate for signing Client Certs + openssl req -x509 -newkey rsa:1024 -nodes \ + -keyout CAkey.pem -out CAcert.pem \ + -sha256 -days 3650 -nodes \ + -subj "/C=UK/ST=London/L=London/O=PAASDEV/CN=broker-ca.local" &> /dev/null + + # Generate the Server Key, and CSR and sign with the CA Certificate + openssl req -new -newkey rsa:1024 -nodes \ + -keyout key.pem -out req.pem \ + -subj "/C=UK/ST=London/L=London/O=PAASDEV/CN=broker.local" &> /dev/null + openssl x509 -req \ + -in req.pem -CA CAcert.pem -CAkey CAkey.pem \ + -extfile <(printf "subjectAltName=IP:127.0.0.1") \ + -CAcreateserial -out cert.pem -days 30 -sha256 #&> /dev/null + + # rm CAkey.pem req.pem + + cert_value="$(sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/\\n/g' "cert.pem")" + key_value="$(sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/\\n/g' "key.pem")" + ca_cert_value="$(sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/\\n/g' "CAcert.pem")" +popd > /dev/null + +echo "TLS Certificates Generated in ${_tmp_dir}" +echo "Example curl command: curl --cacert ${_tmp_dir}/CAcert.pem https://127.0.0.1:3000" + +jq \ + '.tls.certificate = "'"${cert_value}"'" | .tls.private_key = "'"${key_value}"'" | .tls.ca = "'"${ca_cert_value}"'"' \ + "${SCRIPT_DIR}/../config-sample.json" > "${_tmp_dir}/config.json" +go run main.go --config "${_tmp_dir}/config.json" diff --git a/vendor/github.com/onsi/ginkgo/v2/extensions/table/deprecated.go b/vendor/github.com/onsi/ginkgo/v2/extensions/table/deprecated.go deleted file mode 100644 index 6f4be6ab..00000000 --- a/vendor/github.com/onsi/ginkgo/v2/extensions/table/deprecated.go +++ /dev/null @@ -1,3 +0,0 @@ -package table - -type TableSupportHasBeenMovedToTheCoreGinkgoDSL struct{} diff --git a/vendor/modules.txt b/vendor/modules.txt index 81e2c4e2..ec6d0ea6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -92,7 +92,6 @@ github.com/lib/pq/scram ## explicit; go 1.18 github.com/onsi/ginkgo/v2 github.com/onsi/ginkgo/v2/config -github.com/onsi/ginkgo/v2/extensions/table github.com/onsi/ginkgo/v2/formatter github.com/onsi/ginkgo/v2/ginkgo github.com/onsi/ginkgo/v2/ginkgo/build