Add TLS OpenStack API endpoints

This patch provides TLS endpoints secured by a self-signed
certificate. Another patch will provide support for trusted CA-signed
certificates.

A new config.tls.generate-cert option is added that defaults to true.
When true, a self-signed certificate will be generated and OpenStack
API endpoints will be configured to use TLS with that self-signed
certificate. The following config options are added:

snap get microstack config.tls.generate-self-signed
snap get microstack config.tls.cacert-path
snap get microstack config.tls.cert-path
snap get microstack config.tls.key-path

Users can provide their own self-signed certificate by setting
generate-self-signed to false and storing their own certificates/key
at the paths specified by cacert-path, cert-path, and key-path.
'snap set' can also be used to change the cert/key file names.

If using clustering, the certificates/key will be copied from the
control node to the compute nodes. The config for cacert-path,
cert-path, and key-path will be set to the same values as on the
control node.

Other notable changes:
* The existing generate_selfsigned() function is modified to change
  the subject alternative name to be made up of the hostname and
  optionally an IP. The controller hostname and IP are used when
  generating the certificate for self-signed TLS endpoints. The
  hostname is now used instead of 'microstack.run' when generating
  the clustering certificate.
* This change also aligns logging for nginx and corresponding sites
  and moves all nginx sites to {snap_common}/etc/nginx/sites-enabled.

Change-Id: Iceea3127822404a3275fcf8a221cbedc4b52c217
This commit is contained in:
Corey Bryant 2021-03-11 11:58:02 -05:00
parent abf8af66ef
commit 064aae8458
39 changed files with 292 additions and 86 deletions

View File

@ -158,11 +158,11 @@ Answer the questions as follows:
<table>
<tr><td>cloud type:</td> <td><code>openstack</code></td></tr>
<tr><td>endpoint:</td> <td><code>http://10.20.20.1:5000/v3</code></td></tr>
<tr><td>endpoint:</td> <td><code>https://10.20.20.1:5000/v3</code></td></tr>
<tr><td>cert path:</td> <td><code>none</code></td></tr>
<tr><td>auth type:</td> <td><code>userpass</code></td></tr>
<tr><td>region:</td> <td><code>microstack</code></td></tr>
<tr><td>region endpoint:</td> <td><code>http://10.20.20.1:5000/v3</code></td></tr>
<tr><td>region endpoint:</td> <td><code>https://10.20.20.1:5000/v3</code></td></tr>
<tr><td>add another region?:</td> <td><code>N</code></td></tr>
</table>
@ -182,7 +182,7 @@ images in your microstack cloud. Here's how to set that up.
```
mkdir simplestreams
juju metadata generate-image -d ~/simplestreams -i $IMAGE -s bionic -r microstack -u http://10.20.20.1:5000/v3
juju metadata generate-image -d ~/simplestreams -i $IMAGE -s bionic -r microstack -u https://10.20.20.1:5000/v3
```
(If you don't still have an `IMAGE` variable in your env, you can find

View File

@ -35,7 +35,7 @@ def _get_default_config():
'config.network.ext-cidr': '10.20.20.1/24',
'config.network.security-rules': True,
'config.network.dashboard-allowed-hosts': '*',
'config.network.ports.dashboard': 80,
'config.network.ports.dashboard': 443,
'config.network.ports.mysql': 3306,
'config.network.ports.rabbit': 5672,
'config.network.external-bridge-name': 'br-ex',
@ -75,6 +75,14 @@ def _get_default_config():
'config.nova.cpu-mode': 'host-model',
# Do not override cpu-models by default.
'config.nova.cpu-models': '',
'config.tls.generate-self-signed': True,
'config.tls.cacert-path':
f'{snap_common}/etc/ssl/certs/cacert.pem',
'config.tls.cert-path':
f'{snap_common}/etc/ssl/certs/cert.pem',
'config.tls.key-path':
f'{snap_common}/etc/ssl/private/key.pem',
}

View File

@ -8,7 +8,6 @@ setup:
- "{snap_common}/etc/neutron/policy.d"
- "{snap_common}/etc/neutron/rootwrap.d"
- "{snap_common}/etc/nginx/sites-enabled"
- "{snap_common}/etc/nginx/snap/sites-enabled"
- "{snap_common}/etc/glance/glance.conf.d"
- "{snap_common}/etc/placement/placement.conf.d"
- "{snap_common}/etc/horizon/horizon.conf.d"
@ -22,6 +21,8 @@ setup:
- "{snap_common}/etc/cluster/tls"
- "{snap_common}/etc/cluster/uwsgi/snap"
- "{snap_common}/etc/rabbitmq"
- "{snap_common}/etc/ssl/certs"
- "{snap_common}/etc/ssl/private"
- "{snap_common}/fernet-keys"
- "{snap_common}/lib"
- "{snap_common}/lib/images"
@ -33,24 +34,25 @@ setup:
- "{snap_common}/etc/iscsi"
- "{snap_common}/etc/target"
templates:
cluster-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/cluster.conf"
keystone-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/keystone.conf"
cluster-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/cluster.conf"
keystone-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/keystone.conf"
keystone-snap.conf.j2: "{snap_common}/etc/keystone/keystone.conf.d/keystone-snap.conf"
neutron-snap.conf.j2: "{snap_common}/etc/neutron/neutron.conf.d/neutron-snap.conf"
nginx.conf.j2: "{snap_common}/etc/nginx/snap/nginx.conf"
nova-snap.conf.j2: "{snap_common}/etc/nova/nova.conf.d/nova-snap.conf"
nova-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/nova.conf"
nova-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/nova.conf"
glance-snap.conf.j2: "{snap_common}/etc/glance/glance.conf.d/glance-snap.conf"
placement-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/placement.conf"
glance-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/glance.conf"
placement-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/placement.conf"
placement-snap.conf.j2: "{snap_common}/etc/placement/placement.conf.d/placement-snap.conf"
cinder-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/cinder.conf"
cinder-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/cinder.conf"
cinder-snap.conf.j2: "{snap_common}/etc/cinder/cinder.conf.d/cinder-snap.conf"
cinder.database.conf.j2: "{snap_common}/etc/cinder/cinder.conf.d/database.conf"
cinder.rabbitmq.conf.j2: "{snap_common}/etc/cinder/cinder.conf.d/rabbitmq.conf"
cinder.keystone.conf.j2: "{snap_common}/etc/cinder/cinder.conf.d/keystone.conf"
cinder-rootwrap.conf.j2: "{snap_common}/etc/cinder/rootwrap.conf"
horizon-snap.conf.j2: "{snap_common}/etc/horizon/horizon.conf.d/horizon-snap.conf"
horizon-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/horizon.conf"
horizon-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/horizon.conf"
05_snap_tweaks.j2: "{snap_common}/etc/horizon/local_settings.d/_05_snap_tweaks.py"
libvirtd.conf.j2: "{snap_common}/etc/libvirt/libvirtd.conf"
qemu.conf.j2: "{snap_common}/etc/libvirt/qemu.conf"
@ -62,10 +64,12 @@ setup:
nova.conf.d.keystone.conf.j2: "{snap_common}/etc/nova/nova.conf.d/keystone.conf"
nova.conf.d.database.conf.j2: "{snap_common}/etc/nova/nova.conf.d/database.conf"
nova.conf.d.rabbitmq.conf.j2: "{snap_common}/etc/nova/nova.conf.d/rabbitmq.conf"
nova.conf.d.cinder.conf.j2: "{snap_common}/etc/nova/nova.conf.d/cinder.conf"
nova.conf.d.glance.conf.j2: "{snap_common}/etc/nova/nova.conf.d/glance.conf"
nova.conf.d.neutron.conf.j2: "{snap_common}/etc/nova/nova.conf.d/neutron.conf"
nova.conf.d.placement.conf.j2: "{snap_common}/etc/nova/nova.conf.d/placement.conf"
nova.conf.d.console.conf.j2: "{snap_common}/etc/nova/nova.conf.d/console.conf"
nova-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/nova.conf"
keystone.database.conf.j2: "{snap_common}/etc/keystone/keystone.conf.d/database.conf"
glance.database.conf.j2: "{snap_common}/etc/glance/glance.conf.d/database.conf"
placement.conf.d.database.conf.j2: "{snap_common}/etc/placement/placement.conf.d/database.conf"
@ -75,6 +79,7 @@ setup:
neutron.database.conf.j2: "{snap_common}/etc/neutron/neutron.conf.d/database.conf"
neutron.conf.d.rabbitmq.conf.j2: "{snap_common}/etc/neutron/neutron.conf.d/rabbitmq.conf"
neutron_ovn_metadata_agent.ini.j2: "{snap_common}/etc/neutron/neutron_ovn_metadata_agent.ini"
neutron-nginx.conf.j2: "{snap_common}/etc/nginx/sites-enabled/neutron.conf"
rabbitmq.conf.j2: "{snap_common}/etc/rabbitmq/rabbitmq.config"
iscsid.conf.j2: "{snap_common}/etc/iscsi/iscsid.conf"
lvm.conf.j2: "{snap_common}/etc/lvm/lvm.conf"
@ -84,6 +89,9 @@ setup:
nrpe.cfg.j2: "{snap_common}/etc/nrpe/nrpe-microstack.cfg"
filebeat.yaml.j2: "{snap_common}/etc/filebeat/filebeat-microstack.yaml"
chmod:
"{snap_common}/etc/ssl": 0755
"{snap_common}/etc/ssl/certs": 0755
"{snap_common}/etc/ssl/private": 0700
"{snap_common}/instances": 0755
"{snap_common}/etc/microstack.rc": 0644
"{snap_common}/etc/microstack.json": 0644
@ -124,6 +132,10 @@ setup:
virt_type: 'config.nova.virt-type'
cpu_mode: 'config.nova.cpu-mode'
cpu_models: 'config.nova.cpu-models'
tls_generate_self_signed: 'config.tls.generate-self-signed'
tls_cacert_path: 'config.tls.cacert-path'
tls_cert_path: 'config.tls.cert-path'
tls_key_path: 'config.tls.key-path'
entry_points:
keystone-manage:
binary: "{snap}/bin/keystone-manage"

View File

@ -23,7 +23,7 @@ AVAILABLE_THEMES = [
# Point us at keystone.
OPENSTACK_HOST = "10.20.20.1"
OPENSTACK_KEYSTONE_URL = "http://%s:5000/v3" % OPENSTACK_HOST
OPENSTACK_KEYSTONE_URL = "https://%s:5000/v3" % OPENSTACK_HOST
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"
# Turn off external access for now. (This should be turned on once we
@ -40,4 +40,13 @@ CACHES = {
}
SESSION_ENGINE='django.contrib.sessions.backends.cache'
# SSL config
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
{% if tls_generate_self_signed|lower == "false" %}
# TODO(coreycb): Can we verify cert if self-signed certs
# include a ca-cert and cert?
OPENSTACK_SSL_CACERT = "{{ tls_cacert_path }}"
{% else %}
OPENSTACK_SSL_NO_VERIFY = True
{% endif %}

View File

@ -1,5 +1,9 @@
server {
listen 8776;
listen 8776 ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
location / {

View File

@ -2,8 +2,8 @@
auth_strategy = keystone
[keystone_authtoken]
auth_uri = http://{{ control_ip }}:5000
auth_url = http://{{ control_ip }}:5000
auth_uri = https://{{ control_ip }}:5000/v3
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -11,3 +11,4 @@ user_domain_name = default
project_name = service
username = cinder
password = {{ cinder_password }}
cafile = {{ tls_cacert_path }}

View File

@ -0,0 +1,18 @@
server {
listen 9292 ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
# Disabled for glance image uploads. The maximum size will be
# determined by glance settings.
client_max_body_size 0;
location / {
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9282;
}
}

View File

@ -3,6 +3,7 @@
state_path = {{ snap_common }}/lib
# Log to systemd journal
use_journal = True
bind_port = 9282
log_file = {{ snap_common }}/log/glance.log
debug = {{ logging_debug }}

View File

@ -1,6 +1,6 @@
[keystone_authtoken]
auth_uri = http://{{ control_ip }}:5000
auth_url = http://{{ control_ip }}:5000
auth_uri = https://{{ control_ip }}:5000/v3
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -8,6 +8,7 @@ user_domain_name = default
project_name = service
username = glance
password = {{ glance_password }}
cafile = {{ tls_cacert_path }}
[paste_deploy]
flavor = keystone

View File

@ -1,10 +1,11 @@
# If the OpenStack service has an API that runs behind uwsgi+nginx, you'll need
# to define this template. Be sure to update "listen" with the port number and
# also update "api-name" for the socket.
server {
listen {{ dashboard_port }};
error_log syslog:server=unix:/dev/log;
access_log syslog:server=unix:/dev/log;
listen {{ dashboard_port }} ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
location / {
include {{ snap }}/usr/conf/uwsgi_params;
uwsgi_param SCRIPT_NAME '';

View File

@ -1,7 +1,11 @@
server {
listen 5000;
error_log syslog:server=unix:/dev/log;
access_log syslog:server=unix:/dev/log;
listen 5000 ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
location / {
include {{ snap }}/usr/conf/uwsgi_params;
uwsgi_param SCRIPT_NAME '';

View File

@ -13,12 +13,19 @@
"version": 3
}
},
"auth_url": "http://{{ control_ip }}:5000",
"auth_url": "https://{{ control_ip }}:5000/v3",
"endpoint_type": null,
{% if tls_generate_self_signed|lower == "false" %}
"https_cacert": "{{ tls_cacert_path }}",
"https_cert": "{{ tls_cert_path }}",
"https_key": "{{ tls_key_path }}",
"https_insecure": false,
{% else %}
"https_cacert": "",
"https_cert": "",
"https_insecure": false,
"https_key": "",
"https_insecure": true,
{% endif %}
"profiler_conn_str": null,
"profiler_hmac_key": null,
"region_name": ""

View File

@ -3,7 +3,8 @@ export OS_USER_DOMAIN_NAME=default
export OS_PROJECT_NAME=admin
export OS_USERNAME=admin
export OS_PASSWORD={{ keystone_password }}
export OS_AUTH_URL=http://{{ control_ip }}:5000
export OS_AUTH_PROTOCOL=https
export OS_AUTH_URL=https://{{ control_ip }}:5000/v3
export OS_IDENTITY_API_VERSION=3
export OS_IMAGE_API_VERSION=2
export OS_CACERT={{ tls_cacert_path }}

View File

@ -0,0 +1,15 @@
server {
listen 9696 ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
location / {
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:9686;
}
}

View File

@ -3,6 +3,7 @@
state_path = {{ snap_common }}/lib
# Log to systemd journal
use_journal = True
bind_port = 9686
log_file = {{ snap_common }}/log/neutron.log
debug = {{ logging_debug }}

View File

@ -2,8 +2,8 @@
auth_strategy = keystone
[keystone_authtoken]
auth_uri = http://{{ control_ip }}:5000
auth_url = http://{{ control_ip }}:5000
auth_uri = https://{{ control_ip }}:5000/v3
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -11,3 +11,4 @@ user_domain_name = default
project_name = service
username = neutron
password = {{ neutron_password }}
cafile = {{ tls_cacert_path }}

View File

@ -3,7 +3,7 @@ notify_nova_on_port_status_changes = True
notify_nova_on_port_data_changes = True
[nova]
auth_url = http://{{ control_ip }}:5000
auth_url = https://{{ control_ip }}:5000/v3
auth_type = password
project_domain_name = default
user_domain_name = default
@ -11,3 +11,4 @@ region_name = {{ region_name }}
project_name = service
username = nova
password = {{ nova_password }}
cafile = {{ tls_cacert_path }}

View File

@ -1,5 +1,5 @@
[placement]
auth_url = http://{{ control_ip }}:5000
auth_url = https://{{ control_ip }}:5000/v3
auth_type = password
project_domain_name = default
user_domain_name = default
@ -7,3 +7,4 @@ region_name = {{ region_name }}
project_name = service
username = placement
password = {{ placement_password }}
cafile = {{ tls_cacert_path }}

View File

@ -45,5 +45,5 @@ http {
gzip_disable "msie6";
include {{ snap_common }}/etc/nginx/conf.d/*.conf;
include {{ snap_common }}/etc/nginx/snap/sites-enabled/*;
include {{ snap_common }}/etc/nginx/sites-enabled/*;
}

View File

@ -1,10 +1,15 @@
server {
listen 8778;
listen 8774 ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
location / {
include {{ snap }}/usr/conf/uwsgi_params;
uwsgi_param SCRIPT_NAME '';
uwsgi_pass unix://{{ snap_common }}/run/placement-api.sock;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8764;
}
}

View File

@ -7,6 +7,8 @@ state_path = {{ snap_common }}/lib
# Log to systemd journal
use_journal = True
osapi_compute_listen_port = 8764
# Set a hostname to be an FQDN to avoid issues with port binding for
# which a hostname of a Nova node must match a hostname of an OVN chassis.
host = {{ node_fqdn }}

View File

@ -0,0 +1,5 @@
[cinder]
service_type = volume
service_name = cinder
region_name = {{ region_name }}
cafile = {{ tls_cacert_path }}

View File

@ -2,3 +2,4 @@
service_type = image
service_name = glance
region_name = {{ region_name }}
cafile = {{ tls_cacert_path }}

View File

@ -1,6 +1,6 @@
[keystone_authtoken]
auth_uri = http://{{ control_ip }}:5000
auth_url = http://{{ control_ip }}:5000
auth_uri = https://{{ control_ip }}:5000/v3
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -8,6 +8,7 @@ user_domain_name = default
project_name = service
username = nova
password = {{ nova_password }}
cafile = {{ tls_cacert_path }}
[paste_deploy]
flavor = keystone

View File

@ -1,6 +1,6 @@
[neutron]
url = http://{{ control_ip }}:9696
auth_url = http://{{ control_ip }}:5000
url = https://{{ control_ip }}:9696
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -11,3 +11,4 @@ username = neutron
password = {{ neutron_password }}
service_metadata_proxy = True
metadata_proxy_shared_secret = {{ ovn_metadata_proxy_shared_secret }}
cafile = {{ tls_cacert_path }}

View File

@ -1,6 +1,6 @@
[placement]
auth_uri = http://{{ control_ip }}:5000
auth_url = http://{{ control_ip }}:5000
auth_uri = https://{{ control_ip }}:5000/v3
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -9,3 +9,4 @@ project_name = service
username = nova
password = {{ nova_password }}
region_name = {{ region_name }}
cafile = {{ tls_cacert_path }}

View File

@ -1,7 +1,11 @@
server {
listen 8778;
error_log syslog:server=unix:/dev/log;
access_log syslog:server=unix:/dev/log;
listen 8778 ssl;
ssl_certificate {{ tls_cert_path }};
ssl_certificate_key {{ tls_key_path }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
access_log {{ snap_common }}/log/nginx-access.log;
error_log {{ snap_common }}/log/nginx-error.log;
location / {
include {{ snap }}/usr/conf/uwsgi_params;
uwsgi_param SCRIPT_NAME '';

View File

@ -1,6 +1,6 @@
[keystone_authtoken]
auth_uri = http://{{ control_ip }}:5000
auth_url = http://{{ control_ip }}:5000
auth_uri = https://{{ control_ip }}:5000/v3
auth_url = https://{{ control_ip }}:5000/v3
memcached_servers = {{ compute_ip }}:11211
auth_type = password
project_domain_name = default
@ -8,6 +8,7 @@ user_domain_name = default
project_name = service
username = placement
password = {{ placement_password }}
cafile = {{ tls_cacert_path }}
[paste_deploy]
flavor = keystone

View File

@ -22,7 +22,7 @@ fi
# Add default ports for mysql, rabbit and dashboard services.
# [2019-11-21] build 171 (beta) -> master
if [ -z "$(snapctl get config.network.ports.dashboard)" ]; then
snapctl set config.network.ports.dashboard=80
snapctl set config.network.ports.dashboard=443
fi
if [ -z "$(snapctl get config.network.ports.mysql)" ]; then

View File

@ -190,7 +190,7 @@ echo "++++++++++++++++++++++++++++++++++++++++++++++++++"
export HORIZON_IP
if [[ $PREFIX == *"multipass"* ]]; then
echo "Opening $HORIZON_IP:80 up to the outside world."
echo "Opening $HORIZON_IP:443 up to the outside world."
cat<<EOF > /tmp/_10_hosts.py
# Allow all hosts to connect to this machine
ALLOWED_HOSTS = ['*',]

View File

@ -26,9 +26,9 @@ sudo systemctl restart snap.microstack.*
microstack.openstack user show admin || {
sudo microstack.keystone-manage bootstrap \
--bootstrap-password $OS_PASSWORD \
--bootstrap-admin-url http://10.20.20.1:5000/v3/ \
--bootstrap-internal-url http://10.20.20.1:5000/v3/ \
--bootstrap-public-url http://10.20.20.1:5000/v3/ \
--bootstrap-admin-url https://10.20.20.1:5000/v3/ \
--bootstrap-internal-url https://10.20.20.1:5000/v3/ \
--bootstrap-public-url https://10.20.20.1:5000/v3/ \
--bootstrap-region-id microstack
}

View File

@ -434,7 +434,7 @@ class Framework(unittest.TestCase):
'microstack',
'config.credentials.keystone-password'
]).decode('utf-8')
self.driver.get(f'http://{control_ip}:{dashboard_port}/')
self.driver.get(f'https://{control_ip}:{dashboard_port}/')
# Login to horizon!
self.driver.find_element(By.ID, "id_username").click()
self.driver.find_element(By.ID, "id_username").send_keys("admin")

View File

@ -27,16 +27,26 @@ VALIDITY_PERIOD = relativedelta(minutes=20)
def _create_credential():
project_name = 'service'
domain_name = 'default'
# TODO: add support for TLS-terminated Keystone once this is supported.
auth = v3.password.Password(
auth_url="http://localhost:5000/v3",
auth_url="https://localhost:5000/v3",
username='nova',
password=config_get('config.credentials.nova-password'),
user_domain_name=domain_name,
project_domain_name=domain_name,
project_name=project_name
)
sess = session.Session(auth=auth)
if config_get('config.tls.generate-self-signed'):
# TODO(coreycb): Can we verify cert if self-signed certs
# include a ca-cert and cert?
sess = session.Session(
auth=auth,
verify=False,
)
else:
sess = session.Session(
auth=auth,
verify=config_get('config.tls.cacert-path'),
)
keystone_client = client.Client(session=sess)
# Only allow this credential to list the Keystone catalog. After it

View File

@ -98,6 +98,19 @@ def join():
control_ip = response_dict['config']['network']['control-ip']
shell.config_set(**{'config.network.control-ip': control_ip})
# Write controller's TLS certificate data to compute node
tls_path_map = {
'cacert-path': 'tls_cacert',
'cert-path': 'tls_cert',
'key-path': 'tls_key',
}
for tls_config, tls_file in tls_path_map.items():
tls_path = response_dict['config']['tls'][tls_config]
shell.config_set(**{'config.tls.{}'.format(tls_config): tls_path})
with open(tls_path, "w") as f:
f.write(response_dict[tls_file])
shell.config_set(**{'config.tls.generate-self-signed': False})
if __name__ == '__main__':
join()

View File

@ -10,6 +10,7 @@ from werkzeug.exceptions import BadRequest
from cluster.shell import check_output
from cluster.shell import config_get
from keystoneauth1.identity import v3
from keystoneauth1 import session
@ -153,8 +154,20 @@ def handle_unexpected_error(error):
def join_info():
"""Generate the configuration information to return to a client."""
# TODO: be selective about what we return. For now, we just get everything.
info = {}
config = json.loads(check_output('snapctl', 'get', 'config'))
info = {'config': config}
info['config'] = config
# Add the controller's TLS certificate data
tls_path_map = {
'cacert-path': 'tls_cacert',
'cert-path': 'tls_cert',
'key-path': 'tls_key',
}
for tls_config, tls_file in tls_path_map.items():
with open(config_get('config.tls.{}'.format(tls_config)), "r") as f:
info[tls_file] = f.read()
return info
@ -210,8 +223,7 @@ def join():
' authentication data in the request.')
return MissingAuthDataInRequest()
# TODO: handle https here when TLS termination support is added.
keystone_base_url = 'http://localhost:5000/v3'
keystone_base_url = 'https://localhost:5000/v3'
# In an unlikely event of failing to construct an auth object
# treat it as if invalid data got passed in terms of responding
@ -231,7 +243,18 @@ def join():
try:
# Use the auth object with the app credential to create a session
# which the Keystone client will use.
sess = session.Session(auth=auth)
if config_get('config.tls.generate-self-signed'):
# TODO(coreycb): Can we verify cert if self-signed certs
# include a ca-cert and cert?
sess = session.Session(
auth=auth,
verify=False,
)
else:
sess = session.Session(
auth=auth,
verify=config_get('config.tls.cacert-path'),
)
except Exception:
logger.exception('An exception has occurred while trying to build'
' a Session object with auth data'

View File

@ -182,6 +182,7 @@ def init() -> None:
# The following are not yet implemented:
# questions.VmSwappiness(),
# questions.FileHandleLimits(),
questions.TlsCertificates(),
questions.DashboardAccess(),
questions.RabbitMq(),
questions.DatabaseSetup(),

View File

@ -26,14 +26,16 @@ limitations under the License.
import json
from time import sleep
from os import path
from pathlib import Path
from shutil import copyfile
from init import shell
from init.shell import (check, call, check_output, sql, nc_wait, log_wait,
restart, download, disable, enable)
from init.config import Env, log
from init import cluster_tls
from init import tls
from init.questions.question import Question
from init.questions import clustering, network, uninstall # noqa F401
from init.questions import clustering, network, tls, uninstall # noqa F401
_env = Env().get_env()
@ -102,7 +104,13 @@ class Clustering(Question):
'config.services.hypervisor': 'true',
})
# Generate a self-signed certificate for the clustering service.
cluster_tls.generate_selfsigned()
cert_path, key_path = (
Path(shell.config_get('config.cluster.tls-cert-path')),
Path(shell.config_get('config.cluster.tls-key-path')),
)
tls.generate_self_signed(
cert_path, key_path,
fingerprint_config='config.cluster.fingerprint')
# Write templates
check('snap-openstack', 'setup')
@ -288,6 +296,37 @@ class DashboardAccess(ConfigQuestion):
hosts=answer))
class TlsCertificates(Question):
_type = 'boolean'
_question = 'Do you wish to generate a self-signed certificate for TLS?'
config_key = 'config.tls.generate-self-signed'
def yes(self, answer: str) -> None:
role = shell.config_get('config.cluster.role')
if role == 'control':
log.info('Generating TLS Certificate and Key')
cert_path, key_path = (
Path(shell.config_get('config.tls.cert-path')),
Path(shell.config_get('config.tls.key-path')),
)
tls.generate_self_signed(cert_path, key_path,
ip=_env['control_ip'])
copyfile(Path(shell.config_get('config.tls.cert-path')),
Path(shell.config_get('config.tls.cacert-path')))
restart('nginx')
elif role == 'compute':
log.warning('TLS certificate generation can only be performed on '
'control node')
def no(self, answer: str):
log.info('TLS certificates must be provided: config.tls.cacert-path, '
'config.tls.cert-path, and config.tls.key-path.')
restart('nginx')
class RabbitMq(Question):
"""Wait for Rabbit to start, then setup permissions."""
@ -362,7 +401,7 @@ class DatabaseSetup(Question):
if call('openstack', 'user', 'show', 'admin'):
return
bootstrap_url = 'http://{control_ip}:5000/v3/'.format(**_env)
bootstrap_url = 'https://{control_ip}:5000/v3/'.format(**_env)
check('snap-openstack', 'launch', 'keystone-manage', 'bootstrap',
'--bootstrap-password', _env['keystone_password'],
@ -401,7 +440,7 @@ class DatabaseSetup(Question):
log.info('Creating service project ...')
if not call('openstack', 'project', 'show', 'service'):
check('openstack', 'project', 'create', '--domain',
'default', '--description', 'Service Project',
'default', '--description', '"Service Project"',
'service')
log.info('Keystone configured!')
@ -536,7 +575,7 @@ class PlacementSetup(Question):
for endpoint in ['public', 'internal', 'admin']:
call('openstack', 'endpoint', 'create', '--region',
'microstack', 'placement', endpoint,
'http://{control_ip}:8778'.format(**_env))
'https://{control_ip}:8778'.format(**_env))
log.info('Running Placement DB migrations...')
check('snap-openstack', 'launch', 'placement-manage', 'db', 'sync')
@ -612,6 +651,7 @@ class NovaControlPlane(Question):
enable('nova-api')
restart('nova-compute')
restart('nginx')
for service in [
'nova-api-metadata',
@ -630,7 +670,7 @@ class NovaControlPlane(Question):
for endpoint in ['public', 'internal', 'admin']:
call('openstack', 'endpoint', 'create', '--region',
'microstack', 'compute', endpoint,
'http://{control_ip}:8774/v2.1'.format(**_env))
'https://{control_ip}:8774/v2.1'.format(**_env))
log.info('Creating default flavors...')
@ -682,7 +722,7 @@ class CinderSetup(Question):
check(
'openstack', 'endpoint', 'create', '--region',
'microstack', f'volume{api_version}', endpoint,
f'http://{control_ip}:8776/{api_version}/'
f'https://{control_ip}:8776/{api_version}/'
'$(project_id)s'
)
log.info('Running Cinder DB migrations...')
@ -754,12 +794,13 @@ class NeutronControlPlane(Question):
for endpoint in ['public', 'internal', 'admin']:
call('openstack', 'endpoint', 'create', '--region',
'microstack', 'network', endpoint,
'http://{control_ip}:9696'.format(**_env))
'https://{control_ip}:9696'.format(**_env))
check('snap-openstack', 'launch', 'neutron-db-manage', 'upgrade',
'head')
enable('neutron-api')
enable('neutron-ovn-metadata-agent')
restart('nginx')
nc_wait(_env['control_ip'], '9696')
@ -864,10 +905,11 @@ class GlanceSetup(Question):
for endpoint in ['internal', 'admin', 'public']:
check('openstack', 'endpoint', 'create', '--region',
'microstack', 'image', endpoint,
'http://{compute_ip}:9292'.format(**_env))
'https://{compute_ip}:9292'.format(**_env))
check('snap-openstack', 'launch', 'glance-manage', 'db_sync')
enable('glance-api')
restart('nginx')
nc_wait(_env['compute_ip'], '9292')

View File

@ -1,9 +1,12 @@
#!/usr/bin/env python3
from pathlib import Path
from init.shell import check
from datetime import datetime
from dateutil.relativedelta import relativedelta
import ipaddress
import socket
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
@ -15,7 +18,8 @@ from cryptography.x509.oid import NameOID
from init import shell
def generate_selfsigned():
def generate_self_signed(cert_path, key_path, ip=None,
fingerprint_config=None):
"""Generate a self-signed certificate with associated keys.
The certificate will have a fake CNAME and subjAltName since
@ -28,26 +32,29 @@ def generate_selfsigned():
via a secure channel.
https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning
"""
cert_path, key_path = (
Path(shell.config_get('config.cluster.tls-cert-path')),
Path(shell.config_get('config.cluster.tls-key-path')),
)
# Do not generate a new certificate and key if there is already an existing
# pair. TODO: improve this check and allow renewal.
if cert_path.exists() and key_path.exists():
return
dummy_cn = 'microstack.run'
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend(),
)
cn = socket.gethostname()
common_name = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, dummy_cn)
x509.NameAttribute(NameOID.COMMON_NAME, cn)
])
san = x509.SubjectAlternativeName([x509.DNSName(dummy_cn)])
if ip:
san = x509.SubjectAlternativeName(
[x509.DNSName(cn), x509.IPAddress(ipaddress.ip_address(ip))]
)
else:
san = x509.SubjectAlternativeName([x509.DNSName(cn)])
basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
now = datetime.utcnow()
cert = (
x509.CertificateBuilder()
@ -63,7 +70,8 @@ def generate_selfsigned():
)
cert_fprint = cert.fingerprint(hashes.SHA256()).hex()
shell.config_set(**{'config.cluster.fingerprint': cert_fprint})
if fingerprint_config:
shell.config_set(**{fingerprint_config: cert_fprint})
serialized_cert = cert.public_bytes(encoding=serialization.Encoding.PEM)
serialized_key = key.private_bytes(
@ -73,3 +81,5 @@ def generate_selfsigned():
)
cert_path.write_bytes(serialized_cert)
key_path.write_bytes(serialized_key)
check('chmod', '644', str(cert_path))
check('chmod', '600', str(key_path))

View File

@ -207,8 +207,8 @@ Access it with `ssh -i {key_path} {username}@{ip}`\
gate = check_output('snapctl', 'get', 'config.network.ext-gateway')
port = check_output('snapctl', 'get', 'config.network.ports.dashboard')
print('You can also visit the OpenStack dashboard at http://{}:{}'.format(
gate, port))
print('You can also visit the OpenStack dashboard at https://{}:{}'.format(
gate, port))
def main():