You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by je...@apache.org on 2017/07/19 12:01:25 UTC

[incubator-openwhisk] branch master updated: Support client certificate on cli and nginx (#2427)

This is an automated email from the ASF dual-hosted git repository.

jeremiaswerner pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new 8e6b959  Support client certificate on cli and nginx (#2427)
8e6b959 is described below

commit 8e6b95900086a9cd039dd7c48caabc8163e4369b
Author: ningyougang <41...@qq.com>
AuthorDate: Wed Jul 19 20:01:23 2017 +0800

    Support client certificate on cli and nginx (#2427)
    
    In order to increase the security of auth, it is necessary to add client
    certificate on cli and nginx. So user can use wsk -i property set --cert
    openwhisk-client-cert.pem --key openwhisk-client-key.pem to pass client
    certificate to nginx. If you don't want to use default client certificate
    which system provides, you can create your own client certificate instead
    of them.
---
 ansible/group_vars/all                             |  6 +-
 ansible/roles/nginx/files/genssl.sh                | 75 ++++++++++++++++++----
 ...{openwhisk-key.pem => openwhisk-server-key.pem} |  0
 ansible/roles/nginx/tasks/deploy.yml               |  1 +
 ansible/roles/nginx/templates/nginx.conf.j2        |  3 +-
 ansible/setup.yml                                  |  9 ++-
 ansible/templates/whisk.properties.j2              |  1 +
 docs/cli.md                                        |  8 +++
 tests/src/test/scala/common/Wsk.scala              |  2 +
 .../whisk/core/cli/test/WskBasicUsageTests.scala   | 47 +++++++++++++-
 tools/cli/go-whisk-cli/commands/commands.go        |  5 +-
 tools/cli/go-whisk-cli/commands/flags.go           |  4 ++
 tools/cli/go-whisk-cli/commands/property.go        | 74 ++++++++++++++++++++-
 tools/cli/go-whisk-cli/commands/wsk.go             |  2 +
 .../go-whisk-cli/wski18n/resources/en_US.all.json  | 24 +++++++
 tools/cli/go-whisk/whisk/client.go                 | 17 ++++-
 16 files changed, 252 insertions(+), 26 deletions(-)

diff --git a/ansible/group_vars/all b/ansible/group_vars/all
index 18512b1..7f2f269 100644
--- a/ansible/group_vars/all
+++ b/ansible/group_vars/all
@@ -166,10 +166,12 @@ nginx:
     adminportal: 8443
   ssl:
     path: "{{ openwhisk_home }}/ansible/roles/nginx/files"
-    cert: "openwhisk-cert.pem"
-    key: "openwhisk-key.pem"
+    cert: "openwhisk-server-cert.pem"
+    key: "openwhisk-server-key.pem"
     password_enabled: false
     password_file: "ssl.pass"
+    client_ca_cert: "{{ openwhisk_client_ca_cert | default('openwhisk-client-ca-cert.pem') }}"
+    verify_client: "{{ nginx_ssl_verify_client | default('off') }}"
 
 # These are the variables to define all database relevant settings.
 # The authKeys are the users, that are initially created to use OpenWhisk.
diff --git a/ansible/roles/nginx/files/genssl.sh b/ansible/roles/nginx/files/genssl.sh
index f5dd89f..d78400d 100755
--- a/ansible/roles/nginx/files/genssl.sh
+++ b/ansible/roles/nginx/files/genssl.sh
@@ -8,22 +8,71 @@ if [ "$#" -ne 1 ]; then
     echo "usage: $0 [common name: host or ip]"
 fi
 CN=$1
+TYPE=$2
+PASSWORD="openwhisk"
 
 ## generates a (self-signed) certificate
 
 ## uncomment to regenerate the key
-#openssl genrsa -out "$SCRIPTDIR/openwhisk-key.pem" 2048
+#openssl genrsa -out "$SCRIPTDIR/openwhisk-server-key.pem" 2048
 
-echo generating certificate request
-openssl req -new \
-    -key "$SCRIPTDIR/openwhisk-key.pem" \
-    -nodes \
+if [ "$TYPE" == "server" ]; then
+    echo generating server certificate request
+    openssl req -new \
+        -key "$SCRIPTDIR/openwhisk-server-key.pem" \
+        -nodes \
+        -subj "/C=US/ST=NY/L=Yorktown/O=OpenWhisk/CN=$CN" \
+        -out "$SCRIPTDIR/openwhisk-server-request.csr"
+
+    echo generating self-signed password-less server certificate
+    openssl x509 -req \
+        -in "$SCRIPTDIR/openwhisk-server-request.csr" \
+        -signkey "$SCRIPTDIR/openwhisk-server-key.pem" \
+        -out "$SCRIPTDIR/openwhisk-server-cert.pem" \
+        -days 365
+else
+    echo generating client ca key
+    openssl genrsa -aes256 -passout pass:$PASSWORD -out "$SCRIPTDIR/openwhisk-client-ca-key.pem" 2048
+
+    echo generating client ca request
+    openssl req -new \
+    -key "$SCRIPTDIR/openwhisk-client-ca-key.pem" \
+    -passin pass:$PASSWORD \
     -subj "/C=US/ST=NY/L=Yorktown/O=OpenWhisk/CN=$CN" \
-    -out "$SCRIPTDIR/openwhisk-request.csr"
-
-echo generating self-signed password-less certificate
-openssl x509 -req \
-    -in "$SCRIPTDIR/openwhisk-request.csr" \
-    -signkey "$SCRIPTDIR/openwhisk-key.pem" \
-    -out "$SCRIPTDIR/openwhisk-cert.pem" \
-    -days 365
+    -out "$SCRIPTDIR/openwhisk-client-ca.csr"
+
+    echo generating client ca pem
+    openssl x509 -req \
+    -in "$SCRIPTDIR/openwhisk-client-ca.csr" \
+    -signkey "$SCRIPTDIR/openwhisk-client-ca-key.pem" \
+    -passin pass:$PASSWORD \
+    -out "$SCRIPTDIR/openwhisk-client-ca-cert.pem" \
+    -days 365 -sha1 -extensions v3_ca
+
+    echo generating client key
+    openssl genrsa -aes256 -passout pass:$PASSWORD -out "$SCRIPTDIR/openwhisk-client-key.pem" 2048
+
+    echo generating client certificate csr file
+    openssl req -new \
+    -key "$SCRIPTDIR/openwhisk-client-key.pem" \
+    -passin pass:$PASSWORD \
+    -subj "/C=US/ST=NY/L=Yorktown/O=OpenWhisk/CN=guest" \
+    -out "$SCRIPTDIR/openwhisk-client-certificate-request.csr"
+
+    echo generating self-signed client certificate
+    echo 01 > $SCRIPTDIR/openwhisk-client-ca-cert.srl
+    openssl x509 -req \
+    -in "$SCRIPTDIR/openwhisk-client-certificate-request.csr" \
+    -CA "$SCRIPTDIR/openwhisk-client-ca-cert.pem" \
+    -CAkey "$SCRIPTDIR/openwhisk-client-ca-key.pem" \
+    -CAserial "$SCRIPTDIR/openwhisk-client-ca-cert.srl" \
+    -passin pass:$PASSWORD \
+    -out "$SCRIPTDIR/openwhisk-client-cert.pem" \
+    -days 365 -sha1 -extensions v3_req
+
+    echo remove client key\'s password
+    openssl rsa \
+    -in "$SCRIPTDIR/openwhisk-client-key.pem" \
+    -passin pass:$PASSWORD \
+    -out "$SCRIPTDIR/openwhisk-client-key.pem"
+fi
diff --git a/ansible/roles/nginx/files/openwhisk-key.pem b/ansible/roles/nginx/files/openwhisk-server-key.pem
similarity index 100%
rename from ansible/roles/nginx/files/openwhisk-key.pem
rename to ansible/roles/nginx/files/openwhisk-server-key.pem
diff --git a/ansible/roles/nginx/tasks/deploy.yml b/ansible/roles/nginx/tasks/deploy.yml
index 8fafa24..c5b48f3 100644
--- a/ansible/roles/nginx/tasks/deploy.yml
+++ b/ansible/roles/nginx/tasks/deploy.yml
@@ -18,6 +18,7 @@
   with_items:
         - "{{ nginx.ssl.cert }}"
         - "{{ nginx.ssl.key }}"
+        - "{{ nginx.ssl.client_ca_cert }}"
 
 - name: copy password files for cert from local to remote in nginx config directory
   copy:
diff --git a/ansible/roles/nginx/templates/nginx.conf.j2 b/ansible/roles/nginx/templates/nginx.conf.j2
index 4795746..d8526d7 100644
--- a/ansible/roles/nginx/templates/nginx.conf.j2
+++ b/ansible/roles/nginx/templates/nginx.conf.j2
@@ -45,7 +45,8 @@ http {
         {% if nginx.ssl.password_enabled %}
         ssl_password_file   "/etc/nginx/{{ nginx.ssl.password_file }}";
         {% endif %}
-        ssl_verify_client off;
+        ssl_client_certificate /etc/nginx/{{ nginx.ssl.client_ca_cert }};
+        ssl_verify_client {{ nginx.ssl.verify_client }};
         ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;
         ssl_ciphers RC4:HIGH:!aNULL:!MD5;
         ssl_prefer_server_ciphers on;
diff --git a/ansible/setup.yml b/ansible/setup.yml
index 78e187a..28d02c5 100644
--- a/ansible/setup.yml
+++ b/ansible/setup.yml
@@ -30,7 +30,10 @@
     local_action: template src="db_local.ini.j2" dest="{{ playbook_dir }}/db_local.ini"
     when: not db.stat.exists
 
-  - name: gen untrusted certificate for host
-    local_action: shell "{{ playbook_dir }}/roles/nginx/files/genssl.sh" "*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}"
-    when: nginx.ssl.cert == "openwhisk-cert.pem"
+  - name: gen untrusted server certificate for host
+    local_action: shell "{{ playbook_dir }}/roles/nginx/files/genssl.sh" "*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}" "server"
+    when: nginx.ssl.cert == "openwhisk-server-cert.pem"
     
+  - name: gen untrusted client certificate for host
+    local_action: shell "{{ playbook_dir }}/roles/nginx/files/genssl.sh" "*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}" "client"
+    when: nginx.ssl.client_ca_cert == "openwhisk-client-ca-cert.pem"
diff --git a/ansible/templates/whisk.properties.j2 b/ansible/templates/whisk.properties.j2
index ac65f3a..728bff6 100644
--- a/ansible/templates/whisk.properties.j2
+++ b/ansible/templates/whisk.properties.j2
@@ -10,6 +10,7 @@ whisk.logs.dir={{ whisk_logs_dir }}
 whisk.version.name={{ whisk_version_name }}
 whisk.version.date={{ whisk.version.date }}
 whisk.version.buildno={{ docker.image.tag }}
+whisk.ssl.client.verification={{ nginx.ssl.verify_client }}
 whisk.ssl.cert={{ nginx.ssl.path }}/{{ nginx.ssl.cert }}
 whisk.ssl.key={{ nginx.ssl.path }}/{{ nginx.ssl.key }}
 whisk.ssl.challenge=openwhisk
diff --git a/docs/cli.md b/docs/cli.md
index 76ea5a6..fc8890e 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -52,3 +52,11 @@ After you have configured your environment, you can begin using the OpenWhisk CL
 The CLI can be setup to use an HTTPS proxy. To setup an HTTPS proxy, an environment variable called `HTTPS_PROXY` must be created. The variable must be set to the address of the HTTPS proxy, and its port using the following format:
 `{PROXY IP}:{PROXY PORT}`.
 
+## Configure the CLI to use client certificate
+The CLI has an extra level of security from client to apihost, system provides default client certificate configuration which deployment process generated, then you can refer to below steps to use client certificate:
+* The client certificate verification is off default, you can configure `nginx_ssl_verify_client` to `on` or `optional` to open it for your corresponding environment configuration.
+* Create your own client certificate instead of system provides if you want, after created, you can configure `openwhisk_client_ca_cert` to your own ca cert path for your corresponding environment configuration.
+* Run the follwing command to pass client certificate:
+```
+./bin/wsk property set --cert <client_cert_path> --key <client_key_path>
+```
diff --git a/tests/src/test/scala/common/Wsk.scala b/tests/src/test/scala/common/Wsk.scala
index 32a7482..5b9e403 100644
--- a/tests/src/test/scala/common/Wsk.scala
+++ b/tests/src/test/scala/common/Wsk.scala
@@ -71,6 +71,8 @@ import whisk.utils.retry
 
 case class WskProps(
     authKey: String = WhiskProperties.readAuthKey(WhiskProperties.getAuthFileForTesting),
+    cert: String = WhiskProperties.getFileRelativeToWhiskHome("ansible/roles/nginx/files/openwhisk-client-cert.pem").getAbsolutePath,
+    key: String = WhiskProperties.getFileRelativeToWhiskHome("ansible/roles/nginx/files/openwhisk-client-key.pem").getAbsolutePath ,
     namespace: String = "_",
     apiversion: String = "v1",
     apihost: String = WhiskProperties.getEdgeHost) {
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
index e16285e..4a1be09 100644
--- a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
+++ b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
@@ -112,6 +112,41 @@ class WskBasicUsageTests
         }
     }
 
+    // If client certificate verification is off, should ingore run below tests.
+    if (!WhiskProperties.getProperty("whisk.ssl.client.verification").equals("off")){
+        it should "set valid cert key to get expected success result for client certificate verification" in {
+            val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+            try {
+                val namespace = wsk.namespace.list().stdout.trim.split("\n").last
+                val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+                // Send request to https://<apihost>/api/v1/namespaces, wsk client passes client certificate to nginx, nginx will
+                // verify it by client ca's openwhisk-client-ca-cert.pem
+                val stdout = wsk.cli(Seq("property", "set", "-i", "--apihost", wskprops.apihost, "--auth", wskprops.authKey,
+                    "--cert", wskprops.cert, "--key", wskprops.key, "--namespace", namespace), env = env).stdout
+                stdout should include(s"ok: client cert set")
+                stdout should include(s"ok: client key set")
+                stdout should include(s"ok: whisk auth set")
+                stdout should include(s"ok: whisk API host set to ${wskprops.apihost}")
+                stdout should include(s"ok: whisk namespace set to ${namespace}")
+            } finally {
+                tmpwskprops.delete()
+            }
+        }
+
+        it should "set invalid cert key to get expected exception result for client certificate verification" in {
+            val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+            try {
+                val namespace = wsk.namespace.list().stdout.trim.split("\n").last
+                val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+                val thrown = the [Exception] thrownBy wsk.cli(Seq("property", "set", "-i", "--apihost", wskprops.apihost, "--auth", wskprops.authKey,
+                    "--cert", "invalid-cert.pem", "--key", "invalid-key.pem", "--namespace", namespace), env = env)
+                thrown.getMessage should include ("cannot validate certificate")
+            } finally {
+                tmpwskprops.delete()
+            }
+        }
+    }
+
     it should "ensure default namespace is used when a blank namespace is set" in {
         val tmpwskprops = File.createTempFile("wskprops", ".tmp")
         try {
@@ -169,15 +204,21 @@ class WskBasicUsageTests
     it should "validate default property values" in {
         val tmpwskprops = File.createTempFile("wskprops", ".tmp")
         val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
-        val stdout = wsk.cli(Seq("property", "unset", "--auth", "--apihost", "--apiversion", "--namespace"), env = env).stdout
+        val stdout = wsk.cli(Seq("property", "unset", "--auth", "--cert", "--key", "--apihost", "--apiversion", "--namespace"), env = env).stdout
         try {
             stdout should include regex ("ok: whisk auth unset")
+            stdout should include regex ("ok: client cert unset")
+            stdout should include regex ("ok: client key unset")
             stdout should include regex ("ok: whisk API host unset")
             stdout should include regex ("ok: whisk API version unset")
             stdout should include regex ("ok: whisk namespace unset")
 
             wsk.cli(Seq("property", "get", "--auth"), env = env).
                 stdout should include regex ("""(?i)whisk auth\s*$""") // default = empty string
+            wsk.cli(Seq("property", "get", "--cert"), env = env).
+              stdout should include regex ("""(?i)client cert\s*$""") // default = empty string
+            wsk.cli(Seq("property", "get", "--key"), env = env).
+              stdout should include regex ("""(?i)client key\s*$""") // default = empty string
             wsk.cli(Seq("property", "get", "--apihost"), env = env).
                 stdout should include regex ("""(?i)whisk API host\s*$""") // default = empty string
             wsk.cli(Seq("property", "get", "--namespace"), env = env).
@@ -202,9 +243,11 @@ class WskBasicUsageTests
     it should "set multiple property values with single command" in {
         val tmpwskprops = File.createTempFile("wskprops", ".tmp")
         val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
-        val stdout = wsk.cli(Seq("property", "set", "--auth", "testKey", "--apihost", "openwhisk.ng.bluemix.net", "--apiversion", "v1"), env = env).stdout
+        val stdout = wsk.cli(Seq("property", "set", "--auth", "testKey", "--cert", "cert.pem", "--key", "key.pem", "--apihost", "openwhisk.ng.bluemix.net", "--apiversion", "v1"), env = env).stdout
         try {
             stdout should include regex ("ok: whisk auth set")
+            stdout should include regex ("ok: client cert set")
+            stdout should include regex ("ok: client key set")
             stdout should include regex ("ok: whisk API host set")
             stdout should include regex ("ok: whisk API version set")
             val fileContent = FileUtils.readFileToString(tmpwskprops)
diff --git a/tools/cli/go-whisk-cli/commands/commands.go b/tools/cli/go-whisk-cli/commands/commands.go
index 3041c53..aed1660 100644
--- a/tools/cli/go-whisk-cli/commands/commands.go
+++ b/tools/cli/go-whisk-cli/commands/commands.go
@@ -36,7 +36,8 @@ func setupClientConfig(cmd *cobra.Command, args []string) (error){
 
     // Determine if the parent command will require the API host to be set
     apiHostRequired := (cmd.Parent().Name() == "property" && cmd.Name() == "get" && (flags.property.auth ||
-      flags.property.apihost || flags.property.namespace || flags.property.apiversion || flags.property.cliversion)) ||
+      flags.property.cert || flags.property.key || flags.property.apihost || flags.property.namespace ||
+      flags.property.apiversion || flags.property.cliversion)) ||
       (cmd.Parent().Name() == "property" && cmd.Name() == "set" && (len(flags.property.apihostSet) > 0 ||
         len(flags.property.apiversionSet) > 0 || len(flags.global.auth) > 0)) ||
       (cmd.Parent().Name() == "sdk" && cmd.Name() == "install" && len(args) > 0 && args[0] == "bashauto")
@@ -51,6 +52,8 @@ func setupClientConfig(cmd *cobra.Command, args []string) (error){
     }
 
     clientConfig := &whisk.Config{
+        Cert: Properties.Cert,
+        Key: Properties.Key,
         AuthToken:  Properties.Auth,
         Namespace:  Properties.Namespace,
         BaseURL:    baseURL,
diff --git a/tools/cli/go-whisk-cli/commands/flags.go b/tools/cli/go-whisk-cli/commands/flags.go
index a4fc3c0..d506109 100644
--- a/tools/cli/go-whisk-cli/commands/flags.go
+++ b/tools/cli/go-whisk-cli/commands/flags.go
@@ -39,6 +39,8 @@ type Flags struct {
     global struct {
         verbose     bool
         debug       bool
+        cert        string
+        key         string
         auth        string
         apihost     string
         apiversion  string
@@ -62,6 +64,8 @@ type Flags struct {
     }
 
     property struct {
+        cert            bool
+        key             bool
         auth            bool
         apihost         bool
         apiversion      bool
diff --git a/tools/cli/go-whisk-cli/commands/property.go b/tools/cli/go-whisk-cli/commands/property.go
index 7078cf6..0aba5f4 100644
--- a/tools/cli/go-whisk-cli/commands/property.go
+++ b/tools/cli/go-whisk-cli/commands/property.go
@@ -31,6 +31,8 @@ import (
 )
 
 var Properties struct {
+    Cert       string
+    Key        string
     Auth       string
     APIHost    string
     APIVersion string
@@ -41,6 +43,8 @@ var Properties struct {
     PropsFile  string
 }
 
+const DefaultCert       string = ""
+const DefaultKey        string = ""
 const DefaultAuth       string = ""
 const DefaultAPIHost    string = ""
 const DefaultAPIVersion string = "v1"
@@ -77,6 +81,21 @@ var propertySetCmd = &cobra.Command{
         }
 
         // read in each flag, update if necessary
+        if cert := flags.global.cert; len(cert) > 0 {
+            props["CERT"] = cert
+            client.Config.Cert = cert
+            okMsg += fmt.Sprintf(
+                wski18n.T("{{.ok}} client cert set. Run 'wsk property get --cert' to see the new value.\n",
+                    map[string]interface{}{"ok": color.GreenString("ok:")}))
+        }
+
+        if key := flags.global.key; len(key) > 0 {
+            props["KEY"] = key
+            client.Config.Key = key
+            okMsg += fmt.Sprintf(
+                wski18n.T("{{.ok}} client key set. Run 'wsk property get --key' to see the new value.\n",
+                    map[string]interface{}{"ok": color.GreenString("ok:")}))
+        }
 
         if auth := flags.global.auth; len(auth) > 0 {
             props["AUTH"] = auth
@@ -184,6 +203,20 @@ var propertyUnsetCmd = &cobra.Command{
 
         // read in each flag, update if necessary
 
+        if flags.property.cert {
+            delete(props, "CERT")
+            okMsg += fmt.Sprintf(
+                wski18n.T("{{.ok}} client cert unset.\n",
+                    map[string]interface{}{"ok": color.GreenString("ok:")}))
+        }
+
+        if flags.property.key {
+            delete(props, "KEY")
+            okMsg += fmt.Sprintf(
+                wski18n.T("{{.ok}} client key unset.\n",
+                    map[string]interface{}{"ok": color.GreenString("ok:")}))
+        }
+
         if flags.property.auth {
             delete(props, "AUTH")
             okMsg += fmt.Sprintf(
@@ -255,13 +288,22 @@ var propertyGetCmd = &cobra.Command{
     RunE: func(cmd *cobra.Command, args []string) error {
 
         // If no property is explicitly specified, default to all properties
-        if !(flags.property.all || flags.property.auth ||
+        if !(flags.property.all || flags.property.cert ||
+             flags.property.key || flags.property.auth ||
              flags.property.apiversion || flags.property.cliversion ||
              flags.property.namespace || flags.property.apibuild ||
              flags.property.apihost || flags.property.apibuildno) {
             flags.property.all = true
         }
 
+        if flags.property.all || flags.property.cert {
+            fmt.Fprintf(color.Output, "%s\t\t%s\n", wski18n.T("client cert"), boldString(Properties.Cert))
+        }
+
+        if flags.property.all || flags.property.key {
+            fmt.Fprintf(color.Output, "%s\t\t%s\n", wski18n.T("client key"), boldString(Properties.Key))
+        }
+
         if flags.property.all || flags.property.auth {
             fmt.Fprintf(color.Output, "%s\t\t%s\n", wski18n.T("whisk auth"), boldString(Properties.Auth))
         }
@@ -317,6 +359,8 @@ func init() {
     )
 
     // need to set property flags as booleans instead of strings... perhaps with boolApihost...
+    propertyGetCmd.Flags().BoolVar(&flags.property.cert, "cert", false, wski18n.T("client cert"))
+    propertyGetCmd.Flags().BoolVar(&flags.property.key, "key", false, wski18n.T("client key"))
     propertyGetCmd.Flags().BoolVar(&flags.property.auth, "auth", false, wski18n.T("authorization key"))
     propertyGetCmd.Flags().BoolVar(&flags.property.apihost, "apihost", false, wski18n.T("whisk API host"))
     propertyGetCmd.Flags().BoolVar(&flags.property.apiversion, "apiversion", false, wski18n.T("whisk API version"))
@@ -327,10 +371,14 @@ func init() {
     propertyGetCmd.Flags().BoolVar(&flags.property.all, "all", false, wski18n.T("all properties"))
 
     propertySetCmd.Flags().StringVarP(&flags.global.auth, "auth", "u", "", wski18n.T("authorization `KEY`"))
+    propertySetCmd.Flags().StringVar(&flags.global.cert, "cert", "", wski18n.T("client cert"))
+    propertySetCmd.Flags().StringVar(&flags.global.key, "key", "", wski18n.T("client key"))
     propertySetCmd.Flags().StringVar(&flags.property.apihostSet, "apihost", "", wski18n.T("whisk API `HOST`"))
     propertySetCmd.Flags().StringVar(&flags.property.apiversionSet, "apiversion", "", wski18n.T("whisk API `VERSION`"))
     propertySetCmd.Flags().StringVar(&flags.property.namespaceSet, "namespace", "", wski18n.T("whisk `NAMESPACE`"))
 
+    propertyUnsetCmd.Flags().BoolVar(&flags.property.cert, "cert", false, wski18n.T("client cert"))
+    propertyUnsetCmd.Flags().BoolVar(&flags.property.key, "key", false, wski18n.T("client key"))
     propertyUnsetCmd.Flags().BoolVar(&flags.property.auth, "auth", false, wski18n.T("authorization key"))
     propertyUnsetCmd.Flags().BoolVar(&flags.property.apihost, "apihost", false, wski18n.T("whisk API host"))
     propertyUnsetCmd.Flags().BoolVar(&flags.property.apiversion, "apiversion", false, wski18n.T("whisk API version"))
@@ -339,6 +387,8 @@ func init() {
 }
 
 func SetDefaultProperties() {
+    Properties.Key = DefaultCert
+    Properties.Cert = DefaultKey
     Properties.Auth = DefaultAuth
     Properties.Namespace = DefaultNamespace
     Properties.APIHost = DefaultAPIHost
@@ -399,6 +449,14 @@ func loadProperties() error {
         return werr
     }
 
+    if cert, hasProp := props["CERT"]; hasProp {
+        Properties.Cert = cert
+    }
+
+    if key, hasProp := props["KEY"]; hasProp {
+        Properties.Key = key
+    }
+
     if authToken, hasProp := props["AUTH"]; hasProp {
         Properties.Auth = authToken
     }
@@ -436,6 +494,20 @@ func loadProperties() error {
 
 func parseConfigFlags(cmd *cobra.Command, args []string) error {
 
+    if cert := flags.global.cert; len(cert) > 0 {
+        Properties.Cert = cert
+        if client != nil {
+            client.Config.Cert = cert
+        }
+    }
+
+    if key := flags.global.key; len(key) > 0 {
+        Properties.Key = key
+        if client != nil {
+            client.Config.Key = key
+        }
+    }
+
     if auth := flags.global.auth; len(auth) > 0 {
         Properties.Auth = auth
         if client != nil {
diff --git a/tools/cli/go-whisk-cli/commands/wsk.go b/tools/cli/go-whisk-cli/commands/wsk.go
index f8eab5c..e18b978 100644
--- a/tools/cli/go-whisk-cli/commands/wsk.go
+++ b/tools/cli/go-whisk-cli/commands/wsk.go
@@ -60,6 +60,8 @@ func init() {
 
     WskCmd.PersistentFlags().BoolVarP(&flags.global.verbose, "verbose", "v", false, wski18n.T("verbose output"))
     WskCmd.PersistentFlags().BoolVarP(&flags.global.debug, "debug", "d", false, wski18n.T("debug level output"))
+    WskCmd.PersistentFlags().StringVar(&flags.global.cert, "cert", "", wski18n.T("client cert"))
+    WskCmd.PersistentFlags().StringVar(&flags.global.key, "key", "", wski18n.T("client key"))
     WskCmd.PersistentFlags().StringVarP(&flags.global.auth, "auth", "u", "", wski18n.T("authorization `KEY`"))
     WskCmd.PersistentFlags().StringVar(&flags.global.apihost, "apihost", "", wski18n.T("whisk API `HOST`"))
     WskCmd.PersistentFlags().StringVar(&flags.global.apiversion, "apiversion", "", wski18n.T("whisk API `VERSION`"))
diff --git a/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json b/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json
index 7d3929e..9b513a3 100644
--- a/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json
+++ b/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json
@@ -253,6 +253,14 @@
     "translation": "Unable to set the property value: {{.err}}"
   },
   {
+    "id": "{{.ok}} client cert set. Run 'wsk property get --cert' to see the new value.\n",
+    "translation": "{{.ok}} client cert set. Run 'wsk property get --cert' to see the new value.\n"
+  },
+  {
+    "id": "{{.ok}} client key set. Run 'wsk property get --key' to see the new value.\n",
+    "translation": "{{.ok}} client key set. Run 'wsk property get --key' to see the new value.\n"
+  },
+  {
     "id": "{{.ok}} whisk auth set. Run 'wsk property get --auth' to see the new value.\n",
     "translation": "{{.ok}} whisk auth set. Run 'wsk property get --auth' to see the new value.\n"
   },
@@ -293,6 +301,14 @@
     "translation": "Unable to unset the property value: {{.err}}"
   },
   {
+    "id": "{{.ok}} client cert unset.\n",
+    "translation": "{{.ok}} client cert unset.\n"
+  },
+  {
+    "id": "{{.ok}} client key unset.\n",
+    "translation": "{{.ok}} client key unset.\n"
+  },
+  {
     "id": "{{.ok}} whisk auth unset.\n",
     "translation": "{{.ok}} whisk auth unset.\n"
   },
@@ -321,6 +337,14 @@
     "translation": "get property"
   },
   {
+    "id": "client cert",
+    "translation": "client cert"
+  },
+  {
+    "id": "client key",
+    "translation": "client key"
+  },
+  {
     "id": "whisk auth",
     "translation": "whisk auth"
   },
diff --git a/tools/cli/go-whisk/whisk/client.go b/tools/cli/go-whisk/whisk/client.go
index ab50696..dbe8dd3 100644
--- a/tools/cli/go-whisk/whisk/client.go
+++ b/tools/cli/go-whisk/whisk/client.go
@@ -67,6 +67,8 @@ type Client struct {
 
 type Config struct {
     Namespace 	string // NOTE :: Default is "_"
+    Cert        string
+    Key         string
     AuthToken 	string
     Host		string
     BaseURL   	*url.URL // NOTE :: Default is "openwhisk.ng.bluemix.net"
@@ -81,9 +83,18 @@ func NewClient(httpClient *http.Client, config *Config) (*Client, error) {
     // Disable certificate checking in the dev environment if in insecure mode
     if config.Insecure {
         Debug(DbgInfo, "Disabling certificate checking.\n")
-
-        tlsConfig := &tls.Config{
-            InsecureSkipVerify: true,
+        var tlsConfig *tls.Config
+        if config.Cert != "" && config.Key != "" {
+            if cert, err := tls.LoadX509KeyPair(config.Cert, config.Key); err == nil {
+                tlsConfig = &tls.Config{
+                    Certificates: []tls.Certificate{cert},
+                    InsecureSkipVerify: true,
+                }
+            }
+        }else{
+            tlsConfig = &tls.Config{
+                InsecureSkipVerify: true,
+            }
         }
 
         http.DefaultClient.Transport = &http.Transport{

-- 
To stop receiving notification emails like this one, please contact
['"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>'].