Add ciphers options for listeners and pools

Added an option to specify the TLS ciphers used by listeners
and pools. It is specifed through a string in OpenSSL syntax.

Change-Id: Icead9106b06ae7610301a121e51dc5fe1a0e4056
Co-authored-by: Steven Glasford <stevenglasford@gmail.com>
Story: 2006627
Task: 37193
This commit is contained in:
Luke Tollefson 2020-04-08 15:36:59 -05:00 committed by Gregory Thiemonge
parent 1a29983ebc
commit daae6d1fcd
15 changed files with 174 additions and 13 deletions

View File

@ -185,7 +185,9 @@ def create_listener(request, **kwargs):
timeout_member_connect=data['listener'].get('timeout_member_connect'), timeout_member_connect=data['listener'].get('timeout_member_connect'),
timeout_member_data=data['listener'].get('timeout_member_data'), timeout_member_data=data['listener'].get('timeout_member_data'),
timeout_tcp_inspect=data['listener'].get('timeout_tcp_inspect'), timeout_tcp_inspect=data['listener'].get('timeout_tcp_inspect'),
allowed_cidrs=data['listener'].get('allowed_cidrs') allowed_cidrs=data['listener'].get('allowed_cidrs'),
# Replace empty string by None (uses default tls cipher string)
tls_ciphers=data['listener'].get('tls_ciphers') or None,
) )
if data.get('pool'): if data.get('pool'):
@ -252,7 +254,9 @@ def create_pool(request, **kwargs):
loadbalancer_id=kwargs['loadbalancer_id'], loadbalancer_id=kwargs['loadbalancer_id'],
name=data['pool'].get('name'), name=data['pool'].get('name'),
description=data['pool'].get('description'), description=data['pool'].get('description'),
admin_state_up=data['pool'].get('admin_state_up') admin_state_up=data['pool'].get('admin_state_up'),
# Replace empty string by None (uses default tls cipher string)
tls_ciphers=data['pool'].get('tls_ciphers') or None,
) )
if data.get('members'): if data.get('members'):
@ -458,7 +462,9 @@ def update_listener(request, **kwargs):
timeout_member_connect=data['listener'].get('timeout_member_connect'), timeout_member_connect=data['listener'].get('timeout_member_connect'),
timeout_member_data=data['listener'].get('timeout_member_data'), timeout_member_data=data['listener'].get('timeout_member_data'),
timeout_tcp_inspect=data['listener'].get('timeout_tcp_inspect'), timeout_tcp_inspect=data['listener'].get('timeout_tcp_inspect'),
allowed_cidrs=data['listener'].get('allowed_cidrs') allowed_cidrs=data['listener'].get('allowed_cidrs'),
# Replace empty string by None (uses default tls cipher string)
tls_ciphers=data['listener'].get('tls_ciphers') or None,
) )
if data.get('pool'): if data.get('pool'):
@ -527,7 +533,9 @@ def update_pool(request, **kwargs):
session_persistence=data['pool'].get('session_persistence'), session_persistence=data['pool'].get('session_persistence'),
name=data['pool'].get('name'), name=data['pool'].get('name'),
description=data['pool'].get('description'), description=data['pool'].get('description'),
admin_state_up=data['pool'].get('admin_state_up') admin_state_up=data['pool'].get('admin_state_up'),
# Replace empty string by None (uses default tls cipher string)
tls_ciphers=data['pool'].get('tls_ciphers') or None,
) )
# Assemble the lists of member id's to add and remove, if any exist # Assemble the lists of member id's to add and remove, if any exist

View File

@ -53,7 +53,8 @@
'id', 'name', 'description', 'project_id', 'created_at', 'updated_at', 'id', 'name', 'description', 'project_id', 'created_at', 'updated_at',
'connection_limit', 'insert_headers', 'default_pool_id', 'connection_limit', 'insert_headers', 'default_pool_id',
'timeout_client_data', 'timeout_member_connect', 'timeout_client_data', 'timeout_member_connect',
'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs' 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs',
'tls_ciphers'
]]"> ]]">
</hz-resource-property-list> </hz-resource-property-list>
</div> </div>

View File

@ -185,7 +185,8 @@
timeout_member_connect: gettext('Member Connect Timeout'), timeout_member_connect: gettext('Member Connect Timeout'),
timeout_member_data: gettext('Member Data Timeout'), timeout_member_data: gettext('Member Data Timeout'),
timeout_tcp_inspect: gettext('TCP Inspect Timeout'), timeout_tcp_inspect: gettext('TCP Inspect Timeout'),
load_balancers: gettext('Load Balancers') load_balancers: gettext('Load Balancers'),
tls_ciphers: gettext('TLS Cipher String')
}; };
} }

View File

@ -52,7 +52,7 @@
item="ctrl.pool" item="ctrl.pool"
property-groups="[[ property-groups="[[
'id', 'name', 'description', 'project_id', 'created_at', 'updated_at', 'id', 'name', 'description', 'project_id', 'created_at', 'updated_at',
'session_persistence', 'health_monitor_id']]"> 'session_persistence', 'health_monitor_id', 'tls_ciphers']]">
</hz-resource-property-list> </hz-resource-property-list>
</div> </div>
</uib-tab> </uib-tab>

View File

@ -175,7 +175,8 @@
}, },
loadbalancers: gettext('Load Balancers'), loadbalancers: gettext('Load Balancers'),
listeners: gettext('Listeners'), listeners: gettext('Listeners'),
members: gettext('Members') members: gettext('Members'),
tls_ciphers: gettext('TLS Cipher String')
}; };
} }

View File

@ -48,6 +48,7 @@
'The connection limit must be a number greater than or equal to -1.' 'The connection limit must be a number greater than or equal to -1.'
); );
ctrl.timeoutError = gettext('The timeout must be a number between 0 and 31536000000.'); ctrl.timeoutError = gettext('The timeout must be a number between 0 and 31536000000.');
ctrl.tls_ciphersError = gettext('The cipher string must conform to OpenSSL syntax.');
//////////// ////////////

View File

@ -69,3 +69,13 @@
An empty list means allow from any. An empty list means allow from any.
</translate> </translate>
</p> </p>
<p>
<strong translate>TLS Cipher String:</strong>
<translate>
A string of the allowed ciphers using the OpenSSL syntax. The syntax
is a colon separated list of the chiphers, ex.
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
Note, don't include quotation marks. An empty string sets the default TLS
Cipher String configured in Octavia.
</translate>
</p>

View File

@ -167,6 +167,23 @@
</div> </div>
</div> </div>
<div class="row" ng-if="model.spec.listener.protocol === 'TERMINATED_HTTPS'">
<div class="col-xs-12 col-sm-8 col-md-6">
<div class="form-group"
ng-class="{ 'has-error': listenerDetailsForm.tls_ciphers.$invalid && listenerDetailsForm.tls_ciphers.$dirty }">
<label translate class="control-label" for="tls_ciphers">TLS Cipher String</label>
<textarea name="tls_ciphers" id="tls_ciphers" class="form-control"
ng-model="model.spec.listener.tls_ciphers" ng-pattern="/^([A-Z0-9_-]+:)*[A-Z0-9_-]+$/">
</textarea>
<span class="help-block" ng-show="listenerDetailsForm.tls_ciphers.$invalid && listenerDetailsForm.tls_ciphers.$dirty">
{$ ::ctrl.tls_ciphersError $}
</span>
</div>
</div>
</div>
<h4 translate ng-if="model.spec.listener.protocol === 'HTTP' || model.spec.listener.protocol === 'TERMINATED_HTTPS'">Insert Headers</h4> <h4 translate ng-if="model.spec.listener.protocol === 'HTTP' || model.spec.listener.protocol === 'TERMINATED_HTTPS'">Insert Headers</h4>
<div class="row form-group" ng-if="model.spec.listener.protocol === 'HTTP' || model.spec.listener.protocol === 'TERMINATED_HTTPS'"> <div class="row form-group" ng-if="model.spec.listener.protocol === 'HTTP' || model.spec.listener.protocol === 'TERMINATED_HTTPS'">

View File

@ -170,7 +170,8 @@
timeout_member_connect: 5000, timeout_member_connect: 5000,
timeout_member_data: 50000, timeout_member_data: 50000,
timeout_tcp_inspect: 0, timeout_tcp_inspect: 0,
allowed_cidrs: null allowed_cidrs: null,
tls_ciphers: null
}, },
l7policy: { l7policy: {
id: null, id: null,
@ -201,7 +202,8 @@
type: null, type: null,
cookie_name: null cookie_name: null
}, },
admin_state_up: true admin_state_up: true,
tls_ciphers: null
}, },
monitor: { monitor: {
id: null, id: null,
@ -516,6 +518,7 @@
if (finalSpec.listener.protocol !== 'TERMINATED_HTTPS') { if (finalSpec.listener.protocol !== 'TERMINATED_HTTPS') {
// Remove certificate containers if not using TERMINATED_HTTPS // Remove certificate containers if not using TERMINATED_HTTPS
delete finalSpec.certificates; delete finalSpec.certificates;
delete finalSpec.listener.tls_ciphers;
} else { } else {
var containers = []; var containers = [];
angular.forEach(finalSpec.certificates, function(cert) { angular.forEach(finalSpec.certificates, function(cert) {
@ -803,6 +806,7 @@
spec.timeout_member_data = listener.timeout_member_data; spec.timeout_member_data = listener.timeout_member_data;
spec.timeout_tcp_inspect = listener.timeout_tcp_inspect; spec.timeout_tcp_inspect = listener.timeout_tcp_inspect;
spec.allowed_cidrs = listener.allowed_cidrs; spec.allowed_cidrs = listener.allowed_cidrs;
spec.tls_ciphers = listener.tls_ciphers;
} }
function setL7PolicySpec(l7policy) { function setL7PolicySpec(l7policy) {
@ -837,6 +841,7 @@
spec.lb_algorithm = pool.lb_algorithm; spec.lb_algorithm = pool.lb_algorithm;
spec.admin_state_up = pool.admin_state_up; spec.admin_state_up = pool.admin_state_up;
spec.session_persistence = pool.session_persistence; spec.session_persistence = pool.session_persistence;
spec.tls_ciphers = pool.tls_ciphers;
} }
function setMembersSpec(membersList) { function setMembersSpec(membersList) {

View File

@ -1298,10 +1298,10 @@
it('has the right number of properties', function() { it('has the right number of properties', function() {
expect(Object.keys(model.spec).length).toBe(11); expect(Object.keys(model.spec).length).toBe(11);
expect(Object.keys(model.spec.loadbalancer).length).toBe(7); expect(Object.keys(model.spec.loadbalancer).length).toBe(7);
expect(Object.keys(model.spec.listener).length).toBe(15); expect(Object.keys(model.spec.listener).length).toBe(16);
expect(Object.keys(model.spec.l7policy).length).toBe(8); expect(Object.keys(model.spec.l7policy).length).toBe(8);
expect(Object.keys(model.spec.l7rule).length).toBe(7); expect(Object.keys(model.spec.l7rule).length).toBe(7);
expect(Object.keys(model.spec.pool).length).toBe(7); expect(Object.keys(model.spec.pool).length).toBe(8);
expect(Object.keys(model.spec.monitor).length).toBe(11); expect(Object.keys(model.spec.monitor).length).toBe(11);
expect(model.spec.members).toEqual([]); expect(model.spec.members).toEqual([]);
}); });

View File

@ -0,0 +1,45 @@
/*
* Copyright 2020 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.project.lbaasv2')
.controller('PoolDetailsController', PoolDetailsController);
PoolDetailsController.$inject = [
'$scope',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc controller
* @name PoolDetailsController
* @description
* The `PoolDetailsController` controller provides functions for
* configuring the pool details step of the LBaaS wizard.
* @param $scope The angular scope object.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function PoolDetailsController($scope, gettext) {
var ctrl = this;
// Error text for invalid fields
ctrl.tls_ciphersError = gettext('The cipher string must conform to OpenSSL syntax.');
}
})();

View File

@ -0,0 +1,40 @@
/*
* Copyright 2020 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
describe('Pool Details Step', function() {
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('PoolDetailsController', function() {
var ctrl, scope;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
ctrl = $controller('PoolDetailsController', {
$scope: scope
});
}));
it('should define error messages for invalid fields', function() {
expect(ctrl.tls_ciphersError).toBeDefined();
});
});
});
})();

View File

@ -42,3 +42,13 @@
</li> </li>
</ul> </ul>
</p> </p>
<p>
<strong translate>TLS Cipher String:</strong>
<translate>
A string of the allowed ciphers using the OpenSSL syntax. The syntax
is a colon separated list of the chiphers, ex.
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
Note, don't include quotation marks. An empty string sets the default TLS
Cipher String configured in Octavia.
</translate>
</p>

View File

@ -1,4 +1,4 @@
<div> <div ng-controller="PoolDetailsController as ctrl">
<p translate>Provide the details for the pool.</p> <p translate>Provide the details for the pool.</p>
<div class="row"> <div class="row">
@ -84,6 +84,23 @@
</div> </div>
<div class="row">
<div class="col-xs-12 col-sm-8 col-md-6">
<div class="form-group"
ng-class="{ 'has-error': poolDetailsForm.tls_ciphers.$invalid && poolDetailsForm.tls_ciphers.$dirty }">
<label translate class="control-label" for="tls_ciphers">TLS Cipher String</label>
<textarea name="tls_ciphers" id="tls_ciphers" class="form-control"
ng-model="model.spec.pool.tls_ciphers" ng-pattern="/^([A-Z0-9_-]+:)*[A-Z0-9_-]+$/">
</textarea>
<span class="help-block" ng-show="poolDetailsForm.tls_ciphers.$invalid && poolDetailsForm.tls_ciphers.$dirty">
{$ ::ctrl.tls_ciphersError $}
</span>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-8 col-md-6"> <div class="col-xs-12 col-sm-8 col-md-6">

View File

@ -0,0 +1,5 @@
---
features:
- |
Added option to specify TLS ciphers for listeners and pools.
The ciphers are represented in OpenSSL syntax.