Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secure the invoker with ssl. #3968

Merged
merged 3 commits into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,8 @@ ansible/roles/nginx/files/*.p12
ansible/roles/nginx/files/*cert.pem
ansible/roles/nginx/files/*p12
ansible/roles/kafka/files/*
ansible/roles/controller/files/*.csr
ansible/roles/controller/files/*.pem
ansible/roles/controller/files/*.key
ansible/roles/controller/files/*.p12
ansible/roles/controller/files/*
ansible/roles/invoker/files/*

# .zip files must be explicited whitelisted
*.zip
Expand Down
12 changes: 4 additions & 8 deletions ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,12 @@ directory before deploying OpenWhisk.
The following step must be executed once per development environment.
It will generate the `hosts` configuration file based on your environment settings.

```
ansible-playbook -i environments/<environment> setup.yml
```

The default configuration does not run multiple instances of core components (e.g., controller, invoker, kafka).
You may elect to enable high-availability (HA) mode by passing tne ansible option `-e mode=HA` when executing this playbook.
This will configure your deployment with multiple instances (e.g., two kafka instancess, and two invokers).

In addition to the host file generation, you need to configure the database for your deployment. This is done
by creating a file `ansible/db_local.ini` to provide the following properties.
by modifying the file `ansible/db_local.ini` to provide the following properties.

```bash
[db_creds]
Expand All @@ -102,7 +98,7 @@ db_host=
db_port=
```

This file is generated automatically if you are using an ephermeral CouchDB instance. Otherwise, you must create it explicitly.
This file is generated automatically for an ephermeral CouchDB instance during `setup.yml`. If you want to use Cloudant, you have to modify the file.
For convenience, you can use shell environment variables that are read by the playbook to generate the required `db_local.ini` file as shown below.

```
Expand All @@ -113,7 +109,7 @@ export OW_DB_PROTOCOL=<your couchdb protocol>
export OW_DB_HOST=<your couchdb host>
export OW_DB_PORT=<your couchdb port>

ansible-playbook -i environments/<environment> couchdb.yml --tags ini
ansible-playbook -i environments/<environment> setup.yml
```

Alternatively, if you want to use Cloudant as your datastore:
Expand All @@ -126,7 +122,7 @@ export OW_DB_PROTOCOL=https
export OW_DB_HOST=<your cloudant user>.cloudant.com
export OW_DB_PORT=443

ansible-playbook -i environments/<environment> couchdb.yml --tags ini
ansible-playbook -i environments/<environment> setup.yml
```

#### Install Prerequisites
Expand Down
12 changes: 0 additions & 12 deletions ansible/couchdb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,6 @@
---
# This playbook deploys a CouchDB for Openwhisk.

- hosts: localhost
tasks:
- name: check if db_local.ini exists?
tags: ini
stat: path="{{ playbook_dir }}/db_local.ini"
register: db_check

- name: prepare db_local.ini
tags: ini
local_action: template src="db_local.ini.j2" dest="{{ playbook_dir }}/db_local.ini"
when: not db_check.stat.exists

- hosts: db
roles:
- couchdb
40 changes: 24 additions & 16 deletions ansible/group_vars/all
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ limits:
firesPerMinute: "{{ limit_fires_per_minute | default(60) }}"
sequenceMaxLength: "{{ limit_sequence_max_length | default(50) }}"

# Moved here to avoid recursions. Please do not use outside of controller-dict.
__controller_ssl_keyPrefix: "controller-"

# port means outer port
controller:
dir:
Expand Down Expand Up @@ -83,28 +86,19 @@ controller:
loglevel: "{{ controller_loglevel | default(whisk_loglevel) | default('INFO') }}"
entitlement:
spi: "{{ controller_entitlement_spi | default('') }}"
protocol: "{{ controllerProtocolForSetup }}"
protocol: "{{ controller_protocol | default('https') }}"
ssl:
cn: openwhisk-controllers
cert: "{{ controller_ca_cert | default('controller-openwhisk-server-cert.pem') }}"
key: "{{ controller_key | default('controller-openwhisk-server-key.pem') }}"
clientAuth: "{{ controller_client_auth | default('true') }}"
keyPrefix: "{{ __controller_ssl_keyPrefix }}"
storeFlavor: PKCS12
clientAuth: "{{ controller_client_auth | default('true') }}"
cert: "{{ __controller_ssl_keyPrefix }}openwhisk-server-cert.pem"
key: "{{ __controller_ssl_keyPrefix }}openwhisk-server-key.pem"
keystore:
password: "{{ controllerKeystorePassword }}"
path: "/conf/{{ controllerKeystoreName }}"
# keystore and truststore are the same as long as controller and nginx share the certificate
truststore:
password: "{{ controllerKeystorePassword }}"
path: "/conf/{{ controllerKeystoreName }}"
password: "openwhisk"
name: "{{ __controller_ssl_keyPrefix }}openwhisk-keystore.p12"
extraEnv: "{{ controller_extraEnv | default({}) }}"

# move controller protocol outside to not evaluate controller variables during execution of setup.yml
controllerProtocolForSetup: "{{ controller_protocol | default('https') }}"
controllerKeystoreName: "{{ controllerKeyPrefix }}openwhisk-keystore.p12"
controllerKeyPrefix: "controller-"
controllerKeystorePassword: openwhisk

jmx:
basePortController: 15000
rmiBasePortController: 16000
Expand Down Expand Up @@ -166,6 +160,9 @@ zookeeper_connect_string: "{% set ret = [] %}\
invokerHostnameFromMap: "{{ groups['invokers'] | map('extract', hostvars, 'ansible_host') | list | first }}"
invokerHostname: "{{ invokerHostnameFromMap | default(inventory_hostname) }}"

# Moved here to avoid recursions. Please do not use outside of invoker-dict.
__invoker_ssl_keyPrefix: "invoker-"

invoker:
dir:
become: "{{ invoker_dir_become | default(false) }}"
Expand All @@ -187,6 +184,17 @@ invoker:
{{ jmx.jvmCommonArgs }} -Djava.rmi.server.hostname={{ invokerHostname }} -Dcom.sun.management.jmxremote.rmi.port={{ jmx.rmiBasePortInvoker + groups['invokers'].index(inventory_hostname) }} -Dcom.sun.management.jmxremote.port={{ jmx.basePortInvoker + groups['invokers'].index(inventory_hostname) }}
{% endif %}"
extraEnv: "{{ invoker_extraEnv | default({}) }}"
protocol: "{{ invoker_protocol | default('https') }}"
ssl:
cn: "openwhisk-invokers"
keyPrefix: "{{ __invoker_ssl_keyPrefix }}"
storeFlavor: "PKCS12"
clientAuth: "{{ invoker_client_auth | default('true') }}"
cert: "{{ __invoker_ssl_keyPrefix }}openwhisk-server-cert.pem"
key: "{{ __invoker_ssl_keyPrefix }}openwhisk-server-key.pem"
keystore:
password: "{{ invoker_keystore_password | default('openwhisk') }}"
name: "{{ __invoker_ssl_keyPrefix }}openwhisk-keystore.p12"

userLogs:
spi: "{{ userLogs_spi | default('whisk.core.containerpool.logging.DockerToActivationLogStoreProvider') }}"
Expand Down
10 changes: 2 additions & 8 deletions ansible/roles/controller/tasks/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
- name: copy nginx certificate keystore
when: controller.protocol == 'https'
copy:
src: files/{{ controllerKeystoreName }}
src: files/{{ controller.ssl.keystore.name }}
mode: 0666
dest: "{{ controller.confdir }}/{{ controller_name }}"
become: "{{ controller.dir.become }}"
Expand Down Expand Up @@ -207,17 +207,11 @@
"METRICS_LOG": "{{ metrics.log.enabled }}"
"CONFIG_whisk_controller_protocol": "{{ controller.protocol }}"
"CONFIG_whisk_controller_https_keystorePath":
"{{ controller.ssl.keystore.path }}"
"/conf/{{ controller.ssl.keystore.name }}"
"CONFIG_whisk_controller_https_keystorePassword":
"{{ controller.ssl.keystore.password }}"
"CONFIG_whisk_controller_https_keystoreFlavor":
"{{ controller.ssl.storeFlavor }}"
"CONFIG_whisk_controller_https_truststorePath":
"{{ controller.ssl.truststore.path }}"
"CONFIG_whisk_controller_https_truststorePassword":
"{{ controller.ssl.truststore.password }}"
"CONFIG_whisk_controller_https_truststoreFlavor":
"{{ controller.ssl.storeFlavor }}"
"CONFIG_whisk_controller_https_clientAuth":
"{{ controller.ssl.clientAuth }}"
"CONFIG_whisk_loadbalancer_invokerUserMemory":
Expand Down
22 changes: 21 additions & 1 deletion ansible/roles/invoker/tasks/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@
src: "{{ openwhisk_home }}/ansible/roles/kafka/files/{{ kafka.ssl.keystore.name }}"
dest: "{{ invoker.confdir }}/{{ invoker_name }}"

- name: copy keystore, key and cert
when: invoker.protocol == "https"
copy:
src: "{{ item }}"
mode: 0666
dest: "{{ invoker.confdir }}/{{ invoker_name }}"
become: "{{ invoker.dir.become }}"
with_items:
- "files/{{ invoker.ssl.keystore.name }}"
- "files/{{ invoker.ssl.key }}"
- "files/{{ invoker.ssl.cert }}"

- name: check, that required databases exist
include_tasks: "{{ openwhisk_home }}/ansible/tasks/db/checkDb.yml"
vars:
Expand Down Expand Up @@ -224,6 +236,11 @@
"CONFIG_whisk_timeLimit_std": "{{ limit_action_time_std | default() }}"
"CONFIG_whisk_activation_payload_max": "{{ limit_activation_payload | default() }}"
"CONFIG_whisk_transactions_header": "{{ transactions.header }}"
"CONFIG_whisk_invoker_protocol": "{{ invoker.protocol }}"
"CONFIG_whisk_invoker_https_keystorePath": "/conf/{{ invoker.ssl.keystore.name }}"
"CONFIG_whisk_invoker_https_keystorePassword": "{{ invoker.ssl.keystore.password }}"
"CONFIG_whisk_invoker_https_keystoreFlavor": "{{ invoker.ssl.storeFlavor }}"
"CONFIG_whisk_invoker_https_clientAuth": "{{ invoker.ssl.clientAuth }}"

- name: extend invoker dns env
set_fact:
Expand Down Expand Up @@ -290,7 +307,10 @@

- name: wait until Invoker is up and running
uri:
url: "https://{{ ansible_host }}:{{ invoker.port + (invoker_index | int) }}/ping"
url: "{{ invoker.protocol }}:https://{{ ansible_host }}:{{ invoker.port + (invoker_index | int) }}/ping"
validate_certs: "no"
client_key: "{{ invoker.confdir }}/{{ invoker_name }}/{{ invoker.ssl.key }}"
client_cert: "{{ invoker.confdir }}/{{ invoker_name }}/{{ invoker.ssl.cert }}"
register: result
until: result.status == 200
retries: 12
Expand Down
38 changes: 34 additions & 4 deletions ansible/setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

- hosts: localhost
tasks:
# Generate hosts files
- name: gen hosts if 'local' env is used
local_action: template src="{{playbook_dir}}/environments/local/hosts.j2.ini" dest="{{ playbook_dir }}/environments/local/hosts"
when: "'environments/local' in hosts_dir"
Expand All @@ -23,6 +24,21 @@
local_action: template src="{{playbook_dir}}/environments/docker-machine/hosts.j2.ini" dest="{{ playbook_dir }}/environments/docker-machine/hosts"
when: "'environments/docker-machine' in hosts_dir"

- name: Refresh inventory to ensure generated hosts files are used
meta: refresh_inventory

# Generate db_local.ini
- name: check if db_local.ini exists?
tags: ini
stat: path="{{ playbook_dir }}/db_local.ini"
register: db_check

- name: prepare db_local.ini
tags: ini
local_action: template src="db_local.ini.j2" dest="{{ playbook_dir }}/db_local.ini"
when: not db_check.stat.exists

# Generate nginx certificates
- name: gen untrusted server certificate for host
local_action: shell "{{ playbook_dir }}/files/genssl.sh" "*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}" "server" "{{ playbook_dir }}/roles/nginx/files"
when: nginx.ssl.cert == "openwhisk-server-cert.pem"
Expand All @@ -31,6 +47,7 @@
local_action: shell "{{ playbook_dir }}/files/genssl.sh" "*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}" "client" "{{ playbook_dir }}/roles/nginx/files"
when: nginx.ssl.client_ca_cert == "openwhisk-client-ca-cert.pem"

# Generate Kafka certificates
- name: clean up old kafka keystore
file:
path: "{{ playbook_dir }}/roles/kafka/files"
Expand All @@ -46,19 +63,32 @@
become: "{{ logs.dir.become }}"
when: kafka_protocol_for_setup == 'SSL'


- name: generate kafka certificates
local_action: shell "{{ playbook_dir }}/files/genssl.sh" "openwhisk-kafka" "server_with_JKS_keystore" "{{ playbook_dir }}/roles/kafka/files" openwhisk "kafka-" "generateKey"
when: kafka_protocol_for_setup == 'SSL'

# Generate Controller certificates
- name: ensure controller files directory exists
file:
path: "{{ playbook_dir }}/roles/controller/files/"
state: directory
mode: 0777
become: "{{ logs.dir.become }}"
when: controllerProtocolForSetup == 'https'
when: controller.protocol == 'https'

- name: generate controller certificates
when: controllerProtocolForSetup == 'https'
local_action: shell "{{ playbook_dir }}/files/genssl.sh" "openwhisk-controllers" "server" "{{ playbook_dir }}/roles/controller/files" {{ controllerKeystorePassword }} {{ controllerKeyPrefix }} "generateKey"
when: controller.protocol == 'https'
local_action: shell "{{ playbook_dir }}/files/genssl.sh" "{{ controller.ssl.cn }}" "server" "{{ playbook_dir }}/roles/controller/files" {{ controller.ssl.keystore.password }} {{ controller.ssl.keyPrefix }} "generateKey"

# Generate Invoker certificates
- name: ensure invoker files directory exists
file:
path: "{{ playbook_dir }}/roles/invoker/files/"
state: directory
mode: 0777
become: "{{ logs.dir.become }}"
when: invoker.protocol == 'https'

- name: generate invoker certificates
when: invoker.protocol == 'https'
local_action: shell "{{ playbook_dir }}/files/genssl.sh" "{{ invoker.ssl.cn }}" "server" "{{ playbook_dir }}/roles/invoker/files" {{ invoker.ssl.keystore.password }} {{ invoker.ssl.keyPrefix }} "generateKey"
34 changes: 14 additions & 20 deletions common/scala/src/main/scala/whisk/common/Https.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,18 @@ import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
import akka.http.scaladsl.ConnectionContext
import akka.stream.TLSClientAuth
import com.typesafe.sslconfig.akka.AkkaSSLConfig
import whisk.core.WhiskConfig
import pureconfig._

object Https {
case class HttpsConfig(keystorePassword: String,
keystoreFlavor: String,
keystorePath: String,
truststorePath: String,
truststorePassword: String,
truststoreFlavor: String,
clientAuth: String)
private val httpsConfig = loadConfigOrThrow[HttpsConfig]("whisk.controller.https")
case class HttpsConfig(keystorePassword: String, keystoreFlavor: String, keystorePath: String, clientAuth: String)

def getCertStore(password: Array[Char], flavor: String, path: String): KeyStore = {
val certStorePassword: Array[Char] = password
val cs: KeyStore = KeyStore.getInstance(flavor)
val certStore: InputStream = new FileInputStream(path)
cs.load(certStore, certStorePassword)
cs.load(certStore, password)
cs
}

def connectionContext(config: WhiskConfig, sslConfig: Option[AkkaSSLConfig] = None) = {
def connectionContext(httpsConfig: HttpsConfig, sslConfig: Option[AkkaSSLConfig] = None) = {

val keyFactoryType = "SunX509"
val clientAuth = {
Expand All @@ -55,17 +45,21 @@ object Https {
Some(TLSClientAuth.none)
}

// configure keystore
val keystorePassword = httpsConfig.keystorePassword.toCharArray
val ks: KeyStore = getCertStore(keystorePassword, httpsConfig.keystoreFlavor, httpsConfig.keystorePath)

val keyStore: KeyStore = KeyStore.getInstance(httpsConfig.keystoreFlavor)
val keyStoreStream: InputStream = new FileInputStream(httpsConfig.keystorePath)
keyStore.load(keyStoreStream, keystorePassword)

val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(keyFactoryType)
keyManagerFactory.init(ks, keystorePassword)
keyManagerFactory.init(keyStore, keystorePassword)

// configure truststore
val truststorePassword = httpsConfig.truststorePassword.toCharArray
val ts: KeyStore = getCertStore(truststorePassword, httpsConfig.truststoreFlavor, httpsConfig.keystorePath)
// Currently, we are using the keystore as truststore as well, because the clients use the same keys as the
// server for client authentication (if enabled).
// So this code is guided by https://doc.akka.io/docs/akka-http/10.0.9/scala/http/server-side-https-support.html
// This needs to be reworked, when we fix the keys and certificates.
val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(keyFactoryType)
trustManagerFactory.init(ts)
trustManagerFactory.init(keyStore)

val sslContext: SSLContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom)
Expand Down
Loading