From 4d0da0a5b2afa4bc60c46cd564198d931ade8c60 Mon Sep 17 00:00:00 2001 From: Joshua Casey Date: Thu, 3 Aug 2023 16:21:07 -0500 Subject: [PATCH 1/5] Add blog post for v0.25.0 --- ...impersonation-proxy-with-external-certs.md | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md diff --git a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md new file mode 100644 index 00000000..a51db654 --- /dev/null +++ b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md @@ -0,0 +1,337 @@ +--- +title: "Pinniped v0.25.0: With External Certificate Management for the Impersonation Proxy and more" +slug: v0-25-0-external-cert-mgmt-impersonation-proxy +date: 2023-08-09 +author: Joshua T. Casey +image: https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80 +excerpt: "With v0.25.0 you get external certificate management for the impersonation proxy, easier scheduling of the kube-cert-agent, and more" +tags: ['Joshua T. Casey','Ryan Richard', 'Benjamin Petersen', 'release', 'kubernetes', 'pki', 'pinniped', 'tls', 'mtls', 'kind', 'contour', 'cert-manager'] +--- + +![Friendly seal](https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80) +*Photo by [karlheinz_eckhardt Eckhardt](https://unsplash.com/@karlheinz_eckhardt) on [Unsplash](https://unsplash.com/s/photos/seal)* + +With Pinniped v0.25.0 you get the ability to configure an externally-generated certificate for Pinnniped Concierge's impersonation proxy to serve TLS. + +To read more on this feature, and the design decisions behind it, see the [proposal](https://github.com/vmware-tanzu/pinniped/tree/main/proposals/1547_impersonation-proxy-external-certs). +To read more about the impersonation proxy, see the [docs](https://pinniped.dev/docs/reference/supported-clusters/#background). + +To see the feature in practice on a local kind cluster, follow these instructions. +This will perform mTLS between your local client (kubectl and the pinniped CLI) and the impersonation proxy. + +The setup: Using a kind cluster, Contour as an ingress to the impersonation proxy, and `cert-manager` to generate a TLS serving cert. + +```shell +Docker desktop v1.20.1 +Kind v0.20.0 +Contour v1.25.2 +Pinniped v0.25.0 +pinniped CLI v0.25.0 (https://pinniped.dev/docs/howto/install-cli/) +cert-manager v1.12.3 +```` + +Set up kind to run with Contour, using the example kind cluster configuration file provided by Contour. + +```shell +$ wget https://raw.githubusercontent.com/projectcontour/contour/main/examples/kind/kind-expose-port.yaml +$ kind create cluster \ + --config kind-expose-port.yaml \ + --name kind-with-contour \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Install Contour (see https://projectcontour.io/getting-started/ for more details). + +```shell +# From https://projectcontour.io/getting-started/ +$ kubectl apply \ + --filename https://projectcontour.io/quickstart/contour.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +# Verify that the Contour pods are ready +$ kubectl get pods \ + --namespace projectcontour \ + --output wide \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Install Pinniped’s local-user-authenticator and add some sample users (see https://pinniped.dev/docs/tutorials/concierge-only-demo/ for more details). + +```shell +# Install Pinniped's local-user-authenticator +$ kubectl apply \ + --filename https://get.pinniped.dev/v0.25.0/install-local-user-authenticator.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +# Create a local user "pinny" with password "password123" and group "group-for-mtls". +# Each secret in this namespace acts like a user definition. +$ kubectl create secret generic pinny \ + --namespace local-user-authenticator \ + --from-literal=groups=group-for-mtls \ + --from-literal=passwordHash=$(htpasswd -nbBC 10 x password123 | sed -e "s/^x://") \ + --kubeconfig kind-with-contour.kubeconfig.yaml +# We'll need the CA bundle of the local-user-authenticator service to configure the Concierge's WebhookAuthenticator. +# Just make sure this next command does print out the TLS secret, which can take a few seconds to generate. +$ kubectl get secret local-user-authenticator-tls-serving-certificate \ + --namespace local-user-authenticator \ + --output jsonpath={.data.caCertificate} \ + --kubeconfig kind-with-contour.kubeconfig.yaml \ + | tee local-user-authenticator-ca.pem.b64 +``` + +Install Pinniped’s Concierge: + +```shell +$ kubectl apply \ + --filename https://get.pinniped.dev/v0.25.0/install-pinniped-concierge-crds.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml + +$ kubectl apply \ + --filename https://get.pinniped.dev/v0.25.0/install-pinniped-concierge-resources.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Install `cert-manager`: + +```shell +$ kubectl apply \ + --filename https://github.com/cert-manager/cert-manager/releases/download/v1.12.3/cert-manager.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Configure a `cert-manager` certificate for the impersonation proxy to serve TLS. + +Note that this section bootstraps a CA `Issuer` used to issue leaf certificates that can be used to serve TLS. +For more information on this, see the [cert-manager docs](https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers). +The `Certificate` with name `impersonation-serving-cert` will generate the leaf certificate used by the impersonation proxy to serve TLS. + +```shell +$ cat << EOF > self-signed-cert.yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-cluster-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: my-selfsigned-ca + namespace: cert-manager +spec: + isCA: true + commonName: my-selfsigned-ca + secretName: self-signed-ca-for-kind-testing + privateKey: + algorithm: ECDSA + size: 256 + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + group: cert-manager.io +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: my-ca-issuer +spec: + ca: + secretName: self-signed-ca-for-kind-testing +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: impersonation-serving-cert + namespace: pinniped-concierge +spec: + secretName: impersonation-proxy-tls-serving-cert + + duration: 2160h # 90d + renewBefore: 360h # 15d + subject: + organizations: + - Pinniped + isCA: false + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + dnsNames: + - impersonation-proxy-mtls.local + issuerRef: + name: my-ca-issuer + kind: ClusterIssuer + group: cert-manager.io + +EOF + +$ kubectl apply \ + --filename self-signed-cert.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Download the root (self-signed) CA's certificate so that it can be advertised as the CA bundle for the Concierge impersonation proxy: + +```shell +$ kubectl get secret self-signed-ca-for-kind-testing \ + --namespace cert-manager \ + --output jsonpath="{.data.ca\.crt}" \ + --kubeconfig kind-with-contour.kubeconfig.yaml \ + | tee self-signed-ca-for-kind-testing.pem.b64 + +# Tip: Put the contents of self-signed-ca-for-kind-testing.pem.b64 into your copy buffer for a later step! +``` + +Now update the `CredentialIssuer` to use the impersonation proxy (which is disabled on kind by default): + +```shell +$ kubectl edit credentialissuer pinniped-concierge-config \ + --kubeconfig kind-with-contour.kubeconfig.yaml +# Make sure that the spec has the following values: +... + spec: + impersonationProxy: + externalEndpoint: impersonation-proxy-mtls.local + mode: enabled + service: + type: ClusterIP + tls: + certificateAuthorityData: # paste the contents of the file self-signed-ca-for-kind-testing.pem.b64 + secretName: impersonation-proxy-tls-serving-cert +... +# Now save and close the text editor + +# Confirm that the CredentialIssuer looks as expected +$ kubectl get credentialissuers pinniped-concierge-config \ + --output yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +... + spec: + impersonationProxy: + externalEndpoint: impersonation-proxy-mtls.local + mode: enabled + service: + annotations: + # Ignore any annotations + type: ClusterIP + tls: + certificateAuthorityData: LS0tLUJFR0l.......... + secretName: impersonation-proxy-tls-serving-cert +... + +# Confirm that the ClusterIP service for the impersonation proxy was automatically created (may take a minute) +$ kubectl get service pinniped-concierge-impersonation-proxy-cluster-ip \ + --namespace pinniped-concierge \ + --output yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml + +# Configure a webhook authenticator to tell Concierge to validate static tokens using the installed local-user-authenticator +$ cat << EOF > concierge.webhookauthenticator.yaml +apiVersion: authentication.concierge.pinniped.dev/v1alpha1 +kind: WebhookAuthenticator +metadata: + name: local-user-authenticator +spec: + endpoint: https://local-user-authenticator.local-user-authenticator.svc/authenticate + tls: + certificateAuthorityData: $(cat local-user-authenticator-ca.pem.b64) +EOF + +# Create the webhook authenticator +$ kubectl apply \ + --filename concierge.webhookauthenticator.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Now deploy a Contour `HTTPProxy` ingress that fronts the `ClusterIP` service for the impersonation proxy. + +We need to use TLS passthrough in this case, so that the client (kubectl and the pinniped CLI) can establish TLS directly +with the impersonation proxy, and so that client certs used for mTLS will be sent to the impersonation proxy. + +Note in particular the `spec.tcpproxy` block, which is different than the typical `spec.rules` block. +`spec.tcpproxy` is required when using `spec.virtualhost.tls.passthrough: true`. + +See https://projectcontour.io/docs/1.25/config/tls-termination/#tls-session-passthrough for more details. + +```shell +$ cat << EOF > contour-ingress-impersonation-proxy.yaml +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: impersonation-proxy + namespace: pinniped-concierge +spec: + virtualhost: + fqdn: impersonation-proxy-mtls.local + tls: + passthrough: true + tcpproxy: + services: + - name: pinniped-concierge-impersonation-proxy-cluster-ip + port: 443 +EOF + +$ kubectl apply \ + --filename contour-ingress-impersonation-proxy.yaml \ + --kubeconfig kind-with-contour.kubeconfig.yaml +``` + +Now generate the Pinniped kubeconfig so that you can perform mTLS with the impersonation proxy. + +Note that using a static-token does embed those credentials into your kubeconfig. +Never use `local-user-authenticator` in production. + +```shell +# add 127.0.0.1 impersonation-proxy-mtls.local to your /etc/hosts! +$ pinniped get kubeconfig \ + --static-token "pinny:password123" \ + --concierge-authenticator-type webhook \ + --concierge-authenticator-name local-user-authenticator \ + --concierge-mode ImpersonationProxy \ + --kubeconfig kind-with-contour.kubeconfig.yaml \ + > pinniped-kubeconfig.yaml +``` + +Now perform an action as user pinny! + +```shell +$ kubectl get pods -A \ + --kubeconfig pinniped-kubeconfig.yaml +Error from server (Forbidden): pods is forbidden: User "pinny" cannot list resource "pods" in API group "" at the cluster scope: decision made by impersonation-proxy.concierge.pinniped.dev +``` + +This does result in an error because the cluster does not have any `RoleBindings` or `ClusterRoleBindings` that allow your user pinny or the group `group-for-mtls` to perform any actions on the cluster. +Let’s make a `ClusterRoleBinding` that grants this group cluster admin privileges. + +```shell +# Perform this as the cluster admin using the kind kubeconfig +$ kubectl create clusterrolebinding mtls-admins \ + --clusterrole=cluster-admin \ + --group=group-for-mtls \ + --kubeconfig kind-with-contour.kubeconfig.yaml +# Now try again with the Pinniped kubeconfig +$ kubectl get pods -A \ + --kubeconfig pinniped-kubeconfig.yaml +NAMESPACE NAME READY STATUS RESTARTS AGE +pinniped-concierge pinniped-concierge-f4c78b674-bt6zl 1/1 Running 0 3h36m +``` + +Congratulations, you have successfully performed mTLS authentication between your local client (kubectl, using the pinniped CLI) and the impersonation proxy inside the cluster. + +To verify that your username and groups are visible to Kubernetes, run the `pinniped whoami` command. + +```shell +pinniped whoami \ + --kubeconfig pinniped-kubeconfig.yaml +``` + +Now, to verify that the generated kubeconfig `pinniped-kubeconfig.yaml` has the contents of `self-signed-ca-for-kind-testing.pem.b64` as the contained CA bundle for the cluster, +simply cat out both files to compare. \ No newline at end of file From 31c144261f06d1b7f11e5698f3f5542d7fea7cb7 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Mon, 14 Aug 2023 11:37:53 -0400 Subject: [PATCH 2/5] add author to blog list page --- .../scss/site.scss_57bddf56b810cfd059d5f90b3dc00f4f.content | 2 +- site/themes/pinniped/assets/scss/_components.scss | 6 ++++++ site/themes/pinniped/layouts/_default/single.html | 3 --- site/themes/pinniped/layouts/partials/blog-post-card.html | 5 +++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/site/resources/_gen/assets/scss/scss/site.scss_57bddf56b810cfd059d5f90b3dc00f4f.content b/site/resources/_gen/assets/scss/scss/site.scss_57bddf56b810cfd059d5f90b3dc00f4f.content index ecc6c48d..0ac7b2f6 100644 --- a/site/resources/_gen/assets/scss/scss/site.scss_57bddf56b810cfd059d5f90b3dc00f4f.content +++ b/site/resources/_gen/assets/scss/scss/site.scss_57bddf56b810cfd059d5f90b3dc00f4f.content @@ -1,3 +1,3 @@ -body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}header .wrapper{padding:10px 20px}header .desktop-links{float:right;margin:15px 0px 0px 0px;padding-left:0px}header a{color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}header a.active{font-family:"Metropolis-Medium",Helvetica,sans-serif}header li img{vertical-align:bottom;margin-right:10px}header .mobile{display:none}@media only screen and (min-width: 768px) and (max-width: 1279px){header .desktop-links li{padding-right:10px}}@media only screen and (max-width: 767px){header{position:relative}header .expanded-icon{display:none;padding:11px 3px 0px 0px}header .collapsed-icon{padding-top:12px}header .mobile-menu-visible .mobile{display:block}header .mobile-menu-visible .mobile .collapsed-icon{display:none}header .mobile-menu-visible .mobile .expanded-icon{display:block}header .desktop-links{display:none}header .mobile{display:block}header button{float:right}header button:focus{outline:none}header ul{padding-left:0px}header ul li{display:block;margin:20px 0px}header .mobile-menu{position:absolute;background-color:#fff;width:100%;top:70px;left:0px;padding-bottom:20px;display:none}header .mobile-menu .header-links{margin:0px 20px}header .mobile-menu .social{margin:0px 20px;padding-top:20px}header .mobile-menu .social img{vertical-align:middle;padding-right:10px}header .mobile-menu .social a{font-size:14px;padding-right:35px}header .mobile-menu .social a:last-of-type{padding-right:0px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}footer .wrapper{padding:20px}footer li{list-style-type:none;display:inline;padding-right:25px;font-size:12px}footer li:last-of-type{padding-right:0px}footer .top-links{min-height:52px;display:flex;align-items:center;justify-content:space-between}footer .left-links{padding:0px}footer .left-links li img{vertical-align:bottom;margin-right:10px}footer .left-links li a{color:#333;font-weight:300;font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .left-links .mobile{display:none}footer .right-links p{margin:0px}footer .right-links .copywrite{font-size:12px;padding-right:10px}footer .right-links .copywrite a{font-size:12px;color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .right-links a{vertical-align:middle}footer .bottom-links{margin:10px 0px 30px 0px}footer .bottom-links p{font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap}footer .bottom-links p .ot-sdk-show-settings{cursor:pointer}footer .bottom-links a{font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .bottom-links img{max-width:75px;vertical-align:middle;margin-left:30px}@media only screen and (max-width: 767px){footer .footer-links{display:block}footer .footer-links .right-links{display:none}footer .footer-links .left-links{float:none;margin:10px 0px}footer .footer-links .left-links .desktop{display:none}footer .footer-links .left-links .mobile{display:inline}footer .footer-links .left-links .copywrite{display:block;margin-top:20px}footer .bottom-links{margin:10px 0px 20px 0px;float:none}footer .bottom-links img{margin-left:0px;display:block;margin-top:10px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}code{background:#efefef;padding:2px 4px;font-size:85%}pre code{background:none}.highlight pre codesite/sidebar/reorganize{font-size:100%}.hero{background-color:#0091DA;color:#fff}.hero .text-block{max-width:550px;padding:0px 0px 10px 0px}.hero .text-block p{margin-bottom:20px;font-size:18px;color:#fff}.hero h2{font-size:36px}.hero.homepage{background-image:url(/img/hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:80px}@media only screen and (max-width: 767px){.hero .text-block{max-width:unset;margin-right:0px}.hero .button{display:block;text-align:center}.hero.homepage{background-image:none}}.grid-container{margin-top:-80px}.grid-container .grid.three{padding-bottom:20px}.grid-container .grid.three .card{position:relative;padding:30px 20px;background-color:#fff;text-align:center;box-shadow:0px 2px 10px rgba(0,0,0,0.2)}.grid-container .grid.three .card h3{color:#333;font-size:22px}.grid-container .grid.three .card p{color:#333}.introduction .grid.two{column-gap:140px;padding:35px 20px}.introduction .grid.two p{margin:0px;font-size:16px}.introduction .grid.two p.strong{color:#333}@media only screen and (max-width: 767px){.introduction{padding:0px 20px}.introduction .col:first-of-type{padding-bottom:50px}}.use-cases .grid{grid-template-columns:220px 1fr;margin-bottom:30px;grid-template-areas:"image text"}.use-cases .grid .image{background-color:#0091DA;text-align:center;display:flex;align-items:center;justify-content:center;grid-area:image}.use-cases .grid .image img{justify-self:center}.use-cases .grid .text{border:1px solid #F2F2F2;padding:30px;grid-area:text}.use-cases .grid .text a.button{display:block;max-width:138px;text-align:center;padding:5px 10px;min-width:unset}.use-cases .grid.image-right{grid-template-columns:1fr 220px;grid-template-areas:"text image"}@media only screen and (max-width: 767px){.use-cases .grid.image-right{grid-template-columns:1fr;grid-template-areas:"image" "text"}}@media only screen and (max-width: 767px){.use-cases .grid{grid-template-columns:1fr;grid-template-rows:minmax(160px, 1fr);grid-template-areas:"image" "text"}}.use-cases h2{color:#111}.use-cases p.strong{color:#1B3951;font-size:16px}.team{background-color:#1D428A}.team h2,.team h3,.team p{color:#fff}.team p{font-size:16px}.team a{color:#fff;font-weight:300;text-decoration:underline}.team .grid.three{row-gap:40px;margin:40px 0px}.team .bio{display:grid;grid-template-columns:120px 1fr;column-gap:20px}.team .bio .info{align-self:center}.team .bio .info p{margin:0px}.team .bio .info p.name{font-size:16px;font-family:"Metropolis-Medium",Helvetica,sans-serif}.team .bio .info p.position{font-size:14px}.hero.subpage{background-image:url(/img/blog-hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:140px}.experimental .grid.three .col{padding:0px}.experimental .icon{background-color:#0091DA;padding:25px;min-height:95px;display:flex;align-items:center;justify-content:center}.experimental .content{padding:25px}.experimental .content .example{background-color:#F2F2F2}.blog{padding-bottom:50px}.blog .col{border:1px solid #F2F2F2}.blog .col img{width:100%}.blog .col .content{padding:0px 20px}.blog.landing{background-color:#fff;margin-top:-90px}.blog.landing h3 a{font-size:16px}.blog.landing .pagination{margin:30px auto 50px auto}.blog.landing .pagination ul{padding:0px;text-align:center}.blog.landing .pagination ul li{padding:0px}.blog.landing .pagination ul li a{padding:5px 10px}.blog.landing .pagination ul li a.active{background-color:#F2F2F2;border-radius:50%}.blog.landing .pagination ul li.left-arrow{margin-right:15px}.blog.landing .pagination ul li.right-arrow{margin-left:15px}.blog .blog-post{background-color:#fff;margin:-110px 0px 0px -30px;padding:30px 90px 30px 30px}.blog .blog-post .author{color:#0095D3;margin:0px}.blog .blog-post .date{color:#111;margin:0px;font-weight:600}.blog .blog-post .header,.blog .blog-post h4{color:#111;font-weight:600}.blog .blog-post a{font-size:16px}.blog .blog-post ul{list-style-type:disc;padding-left:20px}.blog .blog-post ul li:first-child{margin-top:10px}.blog .blog-post ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;list-style-image:url(/img/arrow.svg)}.blog .blog-post ol li:first-child{margin-top:10px}.blog .blog-post ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.blog .blog-post code .c1{color:#0095D3;font-style:italic}.blog .blog-post code .se{color:#ff0000}.blog .blog-post pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.blog .blog-post img{max-width:100%}.blog .blog-post strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.getting-started{background-color:#F2F2F2;color:#111}.getting-started p{color:#111;font-size:16px}.getting-started .left-side{width:50%;float:left}.getting-started .right-side{width:25%;float:right}.getting-started h2{font-size:30px;margin-bottom:0px}.getting-started a{display:block;max-width:138px;text-align:center;padding:10px;min-width:unset}.getting-started .button{margin-top:50px;border:1px solid #0095D3}@media only screen and (max-width: 767px){.getting-started .wrapper{padding-bottom:40px}.getting-started .left-side{width:100%;float:none}.getting-started .right-side{width:100%;float:none}.getting-started .button{display:block;text-align:center;max-width:unset;margin-top:20px}}.community{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.community .grid .col{border:1px solid #F2F2F2}.community .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.community .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.community .grid .col .content h3{margin-top:0px}.resources{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.resources .embed-responsive{position:relative}.resources .embed-responsive:before{padding-top:56.25%;display:block;content:""}.resources .embed-responsive .embed-responsive-item{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.resources .grid .col{border:1px solid #F2F2F2}.resources .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.resources .grid .col .icon img{max-width:100%;height:auto}.resources .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.resources .grid .col .content h3{margin-top:0px}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.docs .side-nav{width:25%;float:left;position:relative}@media only screen and (max-width: 1279px){.docs .side-nav{width:100%;float:none}}.docs .side-nav a.active{background:#F2F2F2;padding:5px 7px;margin-left:-7px}.docs .side-nav h3{font-size:18px;font-family:"Metropolis-Medium",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav h3 a{font-weight:300;line-height:1.25;color:#000}.docs .side-nav ul{padding-left:0px;margin-top:0;margin-bottom:35px;list-style-type:none}.docs .side-nav ul li{padding-right:0px;display:list-item}.docs .side-nav ul li a{font-size:14px;font-weight:300}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .docs-content{width:75%;float:right}@media only screen and (max-width: 1279px){.docs .docs-content{width:100%;float:none}}.docs .docs-content a{font-size:16px}.docs .docs-content ul{list-style-type:disc;padding-left:20px}.docs .docs-content ul li:first-child{margin-top:10px}.docs .docs-content ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.docs .docs-content ol li:first-child{margin-top:10px}.docs .docs-content ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.docs .docs-content code .c1{color:#0095D3;font-style:italic}.docs .docs-content code .se{color:#ff0000}.docs .docs-content pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.docs .docs-content img{max-width:100%}.docs .docs-content strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.docs .danger{padding:10px;font-family:"Metropolis-LightItalic",Helvetica,sans-serif}.docs .danger .danger-icon{float:left;padding:40px;width:24px;height:24px}.docs .button{white-space:nowrap}.docs .button a{font-size:14px}.docs table td{padding:10px 30px}.chroma{color:#272822;background-color:#fafafa}.chroma .err{color:#960050;background-color:#1e0010}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffffcc}.chroma .lnt{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .ln{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .k{color:#00a8c8}.chroma .kc{color:#00a8c8}.chroma .kd{color:#00a8c8}.chroma .kn{color:#f92672}.chroma .kp{color:#00a8c8}.chroma .kr{color:#00a8c8}.chroma .kt{color:#00a8c8}.chroma .n{color:#111111}.chroma .na{color:#75af00}.chroma .nb{color:#111111}.chroma .bp{color:#111111}.chroma .nc{color:#75af00}.chroma .no{color:#00a8c8}.chroma .nd{color:#75af00}.chroma .ni{color:#111111}.chroma .ne{color:#75af00}.chroma .nf{color:#75af00}.chroma .fm{color:#111111}.chroma .nl{color:#111111}.chroma .nn{color:#111111}.chroma .nx{color:#75af00}.chroma .py{color:#111111}.chroma .nt{color:#f92672}.chroma .nv{color:#111111}.chroma .vc{color:#111111}.chroma .vg{color:#111111}.chroma .vi{color:#111111}.chroma .vm{color:#111111}.chroma .l{color:#ae81ff}.chroma .ld{color:#d88200}.chroma .s{color:#d88200}.chroma .sa{color:#d88200}.chroma .sb{color:#d88200}.chroma .sc{color:#d88200}.chroma .dl{color:#d88200}.chroma .sd{color:#d88200}.chroma .s2{color:#d88200}.chroma .se{color:#8045ff}.chroma .sh{color:#d88200}.chroma .si{color:#d88200}.chroma .sx{color:#d88200}.chroma .sr{color:#d88200}.chroma .s1{color:#d88200}.chroma .ss{color:#d88200}.chroma .m{color:#ae81ff}.chroma .mb{color:#ae81ff}.chroma .mf{color:#ae81ff}.chroma .mh{color:#ae81ff}.chroma .mi{color:#ae81ff}.chroma .il{color:#ae81ff}.chroma .mo{color:#ae81ff}.chroma .o{color:#f92672}.chroma .ow{color:#f92672}.chroma .p{color:#111111}.chroma .c{color:#75715e}.chroma .ch{color:#75715e}.chroma .cm{color:#75715e}.chroma .c1{color:#75715e}.chroma .cs{color:#75715e}.chroma .cp{color:#75715e}.chroma .cpf{color:#75715e}.chroma .ge{font-style:italic}.chroma .gs{font-weight:bold} +body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}header .wrapper{padding:10px 20px}header .desktop-links{float:right;margin:15px 0px 0px 0px;padding-left:0px}header a{color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}header a.active{font-family:"Metropolis-Medium",Helvetica,sans-serif}header li img{vertical-align:bottom;margin-right:10px}header .mobile{display:none}@media only screen and (min-width: 768px) and (max-width: 1279px){header .desktop-links li{padding-right:10px}}@media only screen and (max-width: 767px){header{position:relative}header .expanded-icon{display:none;padding:11px 3px 0px 0px}header .collapsed-icon{padding-top:12px}header .mobile-menu-visible .mobile{display:block}header .mobile-menu-visible .mobile .collapsed-icon{display:none}header .mobile-menu-visible .mobile .expanded-icon{display:block}header .desktop-links{display:none}header .mobile{display:block}header button{float:right}header button:focus{outline:none}header ul{padding-left:0px}header ul li{display:block;margin:20px 0px}header .mobile-menu{position:absolute;background-color:#fff;width:100%;top:70px;left:0px;padding-bottom:20px;display:none}header .mobile-menu .header-links{margin:0px 20px}header .mobile-menu .social{margin:0px 20px;padding-top:20px}header .mobile-menu .social img{vertical-align:middle;padding-right:10px}header .mobile-menu .social a{font-size:14px;padding-right:35px}header .mobile-menu .social a:last-of-type{padding-right:0px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}footer .wrapper{padding:20px}footer li{list-style-type:none;display:inline;padding-right:25px;font-size:12px}footer li:last-of-type{padding-right:0px}footer .top-links{min-height:52px;display:flex;align-items:center;justify-content:space-between}footer .left-links{padding:0px}footer .left-links li img{vertical-align:bottom;margin-right:10px}footer .left-links li a{color:#333;font-weight:300;font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .left-links .mobile{display:none}footer .right-links p{margin:0px}footer .right-links .copywrite{font-size:12px;padding-right:10px}footer .right-links .copywrite a{font-size:12px;color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .right-links a{vertical-align:middle}footer .bottom-links{margin:10px 0px 30px 0px}footer .bottom-links p{font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap}footer .bottom-links p .ot-sdk-show-settings{cursor:pointer}footer .bottom-links a{font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .bottom-links img{max-width:75px;vertical-align:middle;margin-left:30px}@media only screen and (max-width: 767px){footer .footer-links{display:block}footer .footer-links .right-links{display:none}footer .footer-links .left-links{float:none;margin:10px 0px}footer .footer-links .left-links .desktop{display:none}footer .footer-links .left-links .mobile{display:inline}footer .footer-links .left-links .copywrite{display:block;margin-top:20px}footer .bottom-links{margin:10px 0px 20px 0px;float:none}footer .bottom-links img{margin-left:0px;display:block;margin-top:10px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}code{background:#efefef;padding:2px 4px;font-size:85%}pre code{background:none}.highlight pre codesite/sidebar/reorganize{font-size:100%}.hero{background-color:#0091DA;color:#fff}.hero .text-block{max-width:550px;padding:0px 0px 10px 0px}.hero .text-block p{margin-bottom:20px;font-size:18px;color:#fff}.hero h2{font-size:36px}.hero.homepage{background-image:url(/img/hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:80px}@media only screen and (max-width: 767px){.hero .text-block{max-width:unset;margin-right:0px}.hero .button{display:block;text-align:center}.hero.homepage{background-image:none}}.grid-container{margin-top:-80px}.grid-container .grid.three{padding-bottom:20px}.grid-container .grid.three .card{position:relative;padding:30px 20px;background-color:#fff;text-align:center;box-shadow:0px 2px 10px rgba(0,0,0,0.2)}.grid-container .grid.three .card h3{color:#333;font-size:22px}.grid-container .grid.three .card p{color:#333}.introduction .grid.two{column-gap:140px;padding:35px 20px}.introduction .grid.two p{margin:0px;font-size:16px}.introduction .grid.two p.strong{color:#333}@media only screen and (max-width: 767px){.introduction{padding:0px 20px}.introduction .col:first-of-type{padding-bottom:50px}}.use-cases .grid{grid-template-columns:220px 1fr;margin-bottom:30px;grid-template-areas:"image text"}.use-cases .grid .image{background-color:#0091DA;text-align:center;display:flex;align-items:center;justify-content:center;grid-area:image}.use-cases .grid .image img{justify-self:center}.use-cases .grid .text{border:1px solid #F2F2F2;padding:30px;grid-area:text}.use-cases .grid .text a.button{display:block;max-width:138px;text-align:center;padding:5px 10px;min-width:unset}.use-cases .grid.image-right{grid-template-columns:1fr 220px;grid-template-areas:"text image"}@media only screen and (max-width: 767px){.use-cases .grid.image-right{grid-template-columns:1fr;grid-template-areas:"image" "text"}}@media only screen and (max-width: 767px){.use-cases .grid{grid-template-columns:1fr;grid-template-rows:minmax(160px, 1fr);grid-template-areas:"image" "text"}}.use-cases h2{color:#111}.use-cases p.strong{color:#1B3951;font-size:16px}.team{background-color:#1D428A}.team h2,.team h3,.team p{color:#fff}.team p{font-size:16px}.team a{color:#fff;font-weight:300;text-decoration:underline}.team .grid.three{row-gap:40px;margin:40px 0px}.team .bio{display:grid;grid-template-columns:120px 1fr;column-gap:20px}.team .bio .info{align-self:center}.team .bio .info p{margin:0px}.team .bio .info p.name{font-size:16px;font-family:"Metropolis-Medium",Helvetica,sans-serif}.team .bio .info p.position{font-size:14px}.hero.subpage{background-image:url(/img/blog-hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:140px}.experimental .grid.three .col{padding:0px}.experimental .icon{background-color:#0091DA;padding:25px;min-height:95px;display:flex;align-items:center;justify-content:center}.experimental .content{padding:25px}.experimental .content .example{background-color:#F2F2F2}.blog{padding-bottom:50px}.blog .col{border:1px solid #F2F2F2}.blog .col img{width:100%}.blog .col .content{padding:0px 20px}.blog.landing{background-color:#fff;margin-top:-90px}.blog.landing h3 a{font-size:16px}.blog.landing .pagination{margin:30px auto 50px auto}.blog.landing .pagination ul{padding:0px;text-align:center}.blog.landing .pagination ul li{padding:0px}.blog.landing .pagination ul li a{padding:5px 10px}.blog.landing .pagination ul li a.active{background-color:#F2F2F2;border-radius:50%}.blog.landing .pagination ul li.left-arrow{margin-right:15px}.blog.landing .pagination ul li.right-arrow{margin-left:15px}.blog .blog-post{background-color:#fff;margin:-110px 0px 0px -30px;padding:30px 90px 30px 30px}.blog .blog-post .author{color:#0095D3;margin:0px}.blog .blog-post .date{color:#111;margin:0px;font-weight:600}.blog .blog-post .header,.blog .blog-post h4{color:#111;font-weight:600}.blog .blog-post a{font-size:16px}.blog .blog-post ul{list-style-type:disc;padding-left:20px}.blog .blog-post ul li:first-child{margin-top:10px}.blog .blog-post ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;list-style-image:url(/img/arrow.svg)}.blog .blog-post ol li:first-child{margin-top:10px}.blog .blog-post ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.blog .blog-post code .c1{color:#0095D3;font-style:italic}.blog .blog-post code .se{color:#ff0000}.blog .blog-post pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.blog .blog-post img{max-width:100%}.blog .blog-post strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.blog .blog-post-card p.no-margin{margin-block-start:0;margin-block-end:0}.getting-started{background-color:#F2F2F2;color:#111}.getting-started p{color:#111;font-size:16px}.getting-started .left-side{width:50%;float:left}.getting-started .right-side{width:25%;float:right}.getting-started h2{font-size:30px;margin-bottom:0px}.getting-started a{display:block;max-width:138px;text-align:center;padding:10px;min-width:unset}.getting-started .button{margin-top:50px;border:1px solid #0095D3}@media only screen and (max-width: 767px){.getting-started .wrapper{padding-bottom:40px}.getting-started .left-side{width:100%;float:none}.getting-started .right-side{width:100%;float:none}.getting-started .button{display:block;text-align:center;max-width:unset;margin-top:20px}}.community{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.community .grid .col{border:1px solid #F2F2F2}.community .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.community .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.community .grid .col .content h3{margin-top:0px}.resources{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.resources .embed-responsive{position:relative}.resources .embed-responsive:before{padding-top:56.25%;display:block;content:""}.resources .embed-responsive .embed-responsive-item{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.resources .grid .col{border:1px solid #F2F2F2}.resources .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.resources .grid .col .icon img{max-width:100%;height:auto}.resources .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.resources .grid .col .content h3{margin-top:0px}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.docs .side-nav{width:25%;float:left;position:relative}@media only screen and (max-width: 1279px){.docs .side-nav{width:100%;float:none}}.docs .side-nav a.active{background:#F2F2F2;padding:5px 7px;margin-left:-7px}.docs .side-nav h3{font-size:18px;font-family:"Metropolis-Medium",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav h3 a{font-weight:300;line-height:1.25;color:#000}.docs .side-nav ul{padding-left:0px;margin-top:0;margin-bottom:35px;list-style-type:none}.docs .side-nav ul li{padding-right:0px;display:list-item}.docs .side-nav ul li a{font-size:14px;font-weight:300}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .docs-content{width:75%;float:right}@media only screen and (max-width: 1279px){.docs .docs-content{width:100%;float:none}}.docs .docs-content a{font-size:16px}.docs .docs-content ul{list-style-type:disc;padding-left:20px}.docs .docs-content ul li:first-child{margin-top:10px}.docs .docs-content ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.docs .docs-content ol li:first-child{margin-top:10px}.docs .docs-content ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.docs .docs-content code .c1{color:#0095D3;font-style:italic}.docs .docs-content code .se{color:#ff0000}.docs .docs-content pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.docs .docs-content img{max-width:100%}.docs .docs-content strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.docs .danger{padding:10px;font-family:"Metropolis-LightItalic",Helvetica,sans-serif}.docs .danger .danger-icon{float:left;padding:40px;width:24px;height:24px}.docs .button{white-space:nowrap}.docs .button a{font-size:14px}.docs table td{padding:10px 30px}.chroma{color:#272822;background-color:#fafafa}.chroma .err{color:#960050;background-color:#1e0010}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffffcc}.chroma .lnt{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .ln{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .k{color:#00a8c8}.chroma .kc{color:#00a8c8}.chroma .kd{color:#00a8c8}.chroma .kn{color:#f92672}.chroma .kp{color:#00a8c8}.chroma .kr{color:#00a8c8}.chroma .kt{color:#00a8c8}.chroma .n{color:#111111}.chroma .na{color:#75af00}.chroma .nb{color:#111111}.chroma .bp{color:#111111}.chroma .nc{color:#75af00}.chroma .no{color:#00a8c8}.chroma .nd{color:#75af00}.chroma .ni{color:#111111}.chroma .ne{color:#75af00}.chroma .nf{color:#75af00}.chroma .fm{color:#111111}.chroma .nl{color:#111111}.chroma .nn{color:#111111}.chroma .nx{color:#75af00}.chroma .py{color:#111111}.chroma .nt{color:#f92672}.chroma .nv{color:#111111}.chroma .vc{color:#111111}.chroma .vg{color:#111111}.chroma .vi{color:#111111}.chroma .vm{color:#111111}.chroma .l{color:#ae81ff}.chroma .ld{color:#d88200}.chroma .s{color:#d88200}.chroma .sa{color:#d88200}.chroma .sb{color:#d88200}.chroma .sc{color:#d88200}.chroma .dl{color:#d88200}.chroma .sd{color:#d88200}.chroma .s2{color:#d88200}.chroma .se{color:#8045ff}.chroma .sh{color:#d88200}.chroma .si{color:#d88200}.chroma .sx{color:#d88200}.chroma .sr{color:#d88200}.chroma .s1{color:#d88200}.chroma .ss{color:#d88200}.chroma .m{color:#ae81ff}.chroma .mb{color:#ae81ff}.chroma .mf{color:#ae81ff}.chroma .mh{color:#ae81ff}.chroma .mi{color:#ae81ff}.chroma .il{color:#ae81ff}.chroma .mo{color:#ae81ff}.chroma .o{color:#f92672}.chroma .ow{color:#f92672}.chroma .p{color:#111111}.chroma .c{color:#75715e}.chroma .ch{color:#75715e}.chroma .cm{color:#75715e}.chroma .c1{color:#75715e}.chroma .cs{color:#75715e}.chroma .cp{color:#75715e}.chroma .cpf{color:#75715e}.chroma .ge{font-style:italic}.chroma .gs{font-weight:bold} /*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/site/themes/pinniped/assets/scss/_components.scss b/site/themes/pinniped/assets/scss/_components.scss index a09a9f03..64799a91 100644 --- a/site/themes/pinniped/assets/scss/_components.scss +++ b/site/themes/pinniped/assets/scss/_components.scss @@ -326,6 +326,12 @@ pre code { font-family: $metropolis-medium; } } + .blog-post-card { + p.no-margin { + margin-block-start: 0; + margin-block-end: 0; + } + } } .getting-started { diff --git a/site/themes/pinniped/layouts/_default/single.html b/site/themes/pinniped/layouts/_default/single.html index 71b66d04..680aab6c 100644 --- a/site/themes/pinniped/layouts/_default/single.html +++ b/site/themes/pinniped/layouts/_default/single.html @@ -29,6 +29,3 @@ {{ partial "getting-started" . }} {{ end }} - - - diff --git a/site/themes/pinniped/layouts/partials/blog-post-card.html b/site/themes/pinniped/layouts/partials/blog-post-card.html index c4b240f2..eed82844 100644 --- a/site/themes/pinniped/layouts/partials/blog-post-card.html +++ b/site/themes/pinniped/layouts/partials/blog-post-card.html @@ -1,10 +1,11 @@ -
+
{{ .Title }}

{{ .Title }}

+

{{ .Params.author }}

{{ .Params.Excerpt }}

-
\ No newline at end of file +
From b81206c15d4611d42f2cd7de216889d91988c166 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Mon, 14 Aug 2023 11:38:44 -0400 Subject: [PATCH 3/5] blog: impersonation-proxy post updates --- ...impersonation-proxy-with-external-certs.md | 141 ++++++++++++++---- 1 file changed, 114 insertions(+), 27 deletions(-) diff --git a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md index a51db654..726efe42 100644 --- a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md +++ b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md @@ -3,6 +3,9 @@ title: "Pinniped v0.25.0: With External Certificate Management for the Impersona slug: v0-25-0-external-cert-mgmt-impersonation-proxy date: 2023-08-09 author: Joshua T. Casey +authors: +- Joshua T. Casey +- Benjamin A. Petersen image: https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80 excerpt: "With v0.25.0 you get external certificate management for the impersonation proxy, easier scheduling of the kube-cert-agent, and more" tags: ['Joshua T. Casey','Ryan Richard', 'Benjamin Petersen', 'release', 'kubernetes', 'pki', 'pinniped', 'tls', 'mtls', 'kind', 'contour', 'cert-manager'] @@ -11,7 +14,10 @@ tags: ['Joshua T. Casey','Ryan Richard', 'Benjamin Petersen', 'release', 'kubern ![Friendly seal](https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80) *Photo by [karlheinz_eckhardt Eckhardt](https://unsplash.com/@karlheinz_eckhardt) on [Unsplash](https://unsplash.com/s/photos/seal)* -With Pinniped v0.25.0 you get the ability to configure an externally-generated certificate for Pinnniped Concierge's impersonation proxy to serve TLS. +With Pinniped v0.25.0 you get the ability to configure an externally-generated certificate for Pinnniped Concierge's impersonation proxy to serve TLS. The +impersonation proxy is a component within Pinniped that allows the project to support many types of clusters, such as +[Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/), [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine) +and [Azure Kubernetes Service (AKS)](https://azure.microsoft.com/en-us/overview/kubernetes-on-azure). To read more on this feature, and the design decisions behind it, see the [proposal](https://github.com/vmware-tanzu/pinniped/tree/main/proposals/1547_impersonation-proxy-external-certs). To read more about the impersonation proxy, see the [docs](https://pinniped.dev/docs/reference/supported-clusters/#background). @@ -19,11 +25,11 @@ To read more about the impersonation proxy, see the [docs](https://pinniped.dev/ To see the feature in practice on a local kind cluster, follow these instructions. This will perform mTLS between your local client (kubectl and the pinniped CLI) and the impersonation proxy. -The setup: Using a kind cluster, Contour as an ingress to the impersonation proxy, and `cert-manager` to generate a TLS serving cert. +The setup: We will be using a kind cluster, Contour as an ingress to the impersonation proxy, and `cert-manager` to generate a TLS serving cert. ```shell Docker desktop v1.20.1 -Kind v0.20.0 +Kind v0.20.0 Contour v1.25.2 Pinniped v0.25.0 pinniped CLI v0.25.0 (https://pinniped.dev/docs/howto/install-cli/) @@ -34,13 +40,16 @@ Set up kind to run with Contour, using the example kind cluster configuration fi ```shell $ wget https://raw.githubusercontent.com/projectcontour/contour/main/examples/kind/kind-expose-port.yaml +# the --kubeconfig flag on the "create cluster" command will automatically export the kubeconfig file for us $ kind create cluster \ --config kind-expose-port.yaml \ --name kind-with-contour \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Install Contour (see https://projectcontour.io/getting-started/ for more details). +Now we will install Contour (see https://projectcontour.io/getting-started/ for more details). Contour provides our kind +cluster with an Ingress Controller. We will later deploy a Contour HTTPProxy in order to create DNS that we can +use to access the Impersonation Proxy. ```shell # From https://projectcontour.io/getting-started/ @@ -54,7 +63,9 @@ $ kubectl get pods \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Install Pinniped’s local-user-authenticator and add some sample users (see https://pinniped.dev/docs/tutorials/concierge-only-demo/ for more details). +Pinniped's local-user-authenticator will act as a dummy Identity Provider for our example. This resource is not for production +use, but is sufficient for our needs to exercise the new feature of the impersonation proxy. Install Pinniped’s local-user-authenticator +and add some sample users (see https://pinniped.dev/docs/tutorials/concierge-only-demo/ for more details). ```shell # Install Pinniped's local-user-authenticator @@ -77,7 +88,8 @@ $ kubectl get secret local-user-authenticator-tls-serving-certificate \ | tee local-user-authenticator-ca.pem.b64 ``` -Install Pinniped’s Concierge: +In this example, we are only interacting with the Pinniped's Concierge. The Supervisor is not in use as we are not interacting +with a real external OIDC Identity Provider. Install Pinniped's Concierge: ```shell $ kubectl apply \ @@ -89,7 +101,8 @@ $ kubectl apply \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Install `cert-manager`: +To handle X.509 certificate management for us, we will install cert-manager. For the purposes of this exercise, we will use `cert-manager` +to generate our CA certificates as well as our TLS serving certificates. Install `cert-manager`: ```shell $ kubectl apply \ @@ -97,11 +110,29 @@ $ kubectl apply \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Configure a `cert-manager` certificate for the impersonation proxy to serve TLS. +For this demonstration we will be using `cert-manager` to simulate our own Public Key Infrastructure (PKI). +We will create the appropriate CA certificates and TLS serving certificates for the impersonation proxy to serve TLS. +For more information about using `cert-manager` to achieve this, see the [cert-manager docs](https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers). -Note that this section bootstraps a CA `Issuer` used to issue leaf certificates that can be used to serve TLS. -For more information on this, see the [cert-manager docs](https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers). -The `Certificate` with name `impersonation-serving-cert` will generate the leaf certificate used by the impersonation proxy to serve TLS. +In summary, we will do the following: + +- Create two `ClusterIssuer` resources, one named `selfsigned-cluster-issuer` and another named `my-ca-issuer`. +- The `ClusterIssuer` named `my-ca-issuer` will be used to create several `Certificat`e resources. First, we will create + the `Certificate` called `my-selfsigned-ca` (which will reference a `Secret` named `self-signed-ca-for-kind-testing` where + the actual certificate data will be stored). +- We will later retrieve the `Secret` called `self-signed-ca-for-kind-testing` so that we can add the CA to the Pinniped Concierge's + `CredentialIssuer` resource so that it can be advertised and used to verify TLS serving certificates. +- Then, we will create the `ClusterIssuer` called `my-ca-issuer`. We will reference the `Certificate` called `my-selfsigned-ca` via + it's `Secret` named `self-signed-ca-for-kind-testing`. This will allow us to use the CA to sign TLS serving certificates. +- Then, we will use the `ClusterIssuer` called `my-ca-issuer` to generate a `Certificate` that will be a TLS serving certificate + called `impersonation-serving-cert`. As before, the actual certificate data will be stored in a Kubernetes `Secret` which we + will name `impersonation-proxy-tls-serving-cert`. +- Finally, we will update the Pinniped Concierge's `CredentiaIissuer` resource to use the TLS serving certificate stored in the + `Secret` called `impersonation-proxy-tls-serving-cert`. + +If all goes well, the Impersonation Proxy endpoints will be served with a TLS serving certificate that can be validated by the +CA certificate that generated it. That's a lot! Fortunately, the majority of the work is done painlessly via the following +simple commands: ```shell $ cat << EOF > self-signed-cert.yaml @@ -150,7 +181,6 @@ metadata: namespace: pinniped-concierge spec: secretName: impersonation-proxy-tls-serving-cert - duration: 2160h # 90d renewBefore: 360h # 15d subject: @@ -177,7 +207,8 @@ $ kubectl apply \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Download the root (self-signed) CA's certificate so that it can be advertised as the CA bundle for the Concierge impersonation proxy: +Download the root (self-signed) CA's certificate. We will be adding it to the Pinniped Concierge's `CredentialIssuer` resource +in order to configure the impersonation proxy to advertise the certificate as its CA. ```shell $ kubectl get secret self-signed-ca-for-kind-testing \ @@ -189,13 +220,23 @@ $ kubectl get secret self-signed-ca-for-kind-testing \ # Tip: Put the contents of self-signed-ca-for-kind-testing.pem.b64 into your copy buffer for a later step! ``` -Now update the `CredentialIssuer` to use the impersonation proxy (which is disabled on kind by default): +The `CredentialIssuer` resource called `pinniped-concierge-config` already exists. We need to edit it. +Kind clusters have no need to use the impersonation proxy by default (it is designed for public cloud providers), +so we will make several changes to this resource: + +- Set the `spec.impersonationProxy.mode: enabled` +- Set the `spec.impersonationProxy.tls.certificateAuthorityData` to match the certificate named `my-ca-issuer` which + stores its certificate data in the `Secret` called `self-signed-ca-for-kind-testing` (which we previously recorded + in the file `self-signed-ca-for-kind-testing.pem.b64`) ```shell $ kubectl edit credentialissuer pinniped-concierge-config \ --kubeconfig kind-with-contour.kubeconfig.yaml -# Make sure that the spec has the following values: -... +``` + +Make sure that the spec has the following values: + +```yaml spec: impersonationProxy: externalEndpoint: impersonation-proxy-mtls.local @@ -205,14 +246,20 @@ $ kubectl edit credentialissuer pinniped-concierge-config \ tls: certificateAuthorityData: # paste the contents of the file self-signed-ca-for-kind-testing.pem.b64 secretName: impersonation-proxy-tls-serving-cert -... -# Now save and close the text editor +``` +Then save and close the text editor. Once saved, get the resource again and verify that the contents are correct: + +```bash # Confirm that the CredentialIssuer looks as expected $ kubectl get credentialissuers pinniped-concierge-config \ --output yaml \ --kubeconfig kind-with-contour.kubeconfig.yaml -... +``` + +Ensuring the following: + +```yaml spec: impersonationProxy: externalEndpoint: impersonation-proxy-mtls.local @@ -224,14 +271,39 @@ $ kubectl get credentialissuers pinniped-concierge-config \ tls: certificateAuthorityData: LS0tLUJFR0l.......... secretName: impersonation-proxy-tls-serving-cert -... + status: + strategies: + # this strategy should be automatically updated with the configured + # spec.tls.certificateAuthorityData from the previous step + - frontend: + impersonationProxyInfo: + certificateAuthorityData: LS0tLUJFR0l.......... +``` +In the `CredentialIssuer` `status.strategies` there should be a `frontend` strategy with a `impersonationProxyInfo.certificateAuthorityData` +value that matches that of the configured `spec.tls.certificateAuthorityData`. This is how the CredentialIssuer advertises +its CA bundle. + +Next, we review our `Service` configuration. + +```shell # Confirm that the ClusterIP service for the impersonation proxy was automatically created (may take a minute) $ kubectl get service pinniped-concierge-impersonation-proxy-cluster-ip \ --namespace pinniped-concierge \ --output yaml \ --kubeconfig kind-with-contour.kubeconfig.yaml +``` +Configure a webhook authenticator to tell Concierge to validate static tokens using the installed `local-user-authenticator`. +When we installed the Pinniped `local-user-authenticator`, we created a service called `local-user-authenticator` in the +`local-user-authenticator` namespace. We previously retrieved the Secret named `local-user-authenticator-tls-serving-certificate` +so that we could use it to configure this `WebhookAuthenticator` to use that certificate. Note that we did not generate this +certificate via `cert-manager`, this is still a self-signed certificate created by Pinniped. + +The `endpoint` here is referenced via Kubernetes DNS in the format `..svc` targeting the `/authenticate` +endpoint of the `local-user-authenticator`. We will be using https, if course. + +```yaml # Configure a webhook authenticator to tell Concierge to validate static tokens using the installed local-user-authenticator $ cat << EOF > concierge.webhookauthenticator.yaml apiVersion: authentication.concierge.pinniped.dev/v1alpha1 @@ -284,13 +356,24 @@ $ kubectl apply \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Now generate the Pinniped kubeconfig so that you can perform mTLS with the impersonation proxy. +Now to generate the Pinniped kubeconfig so that you can perform mTLS with the impersonation proxy. -Note that using a static-token does embed those credentials into your kubeconfig. -Never use `local-user-authenticator` in production. +Since we are interacting with a kind cluster, we will need to ensure HTTP requests are routed to the cluster. +In this example, we will edit the `/etc/hosts` file to resolve the `impersonation-proxy-mtls.local` to `localhost` via `127.0.0.1`. ```shell -# add 127.0.0.1 impersonation-proxy-mtls.local to your /etc/hosts! +## +# Host Database +127.0.0.1 impersonation-proxy-mtls.local +``` + +Note that using a static-token does embed those credentials into your kubeconfig. This is not suitable for a production +deployment. As we said before, we are using `local-user-authenticator` as a simple Identity Provider for illustrative purposes +only. In a real production use case you would not employ the `--static-token` flag which would ensure credentials are not +embedded in your kubeconfig, an important security feature. Never use `local-user-authenticator` in production. + +```shell +# be sure you added 127.0.0.1 impersonation-proxy-mtls.local to your /etc/hosts! $ pinniped get kubeconfig \ --static-token "pinny:password123" \ --concierge-authenticator-type webhook \ @@ -324,7 +407,8 @@ NAMESPACE NAME pinniped-concierge pinniped-concierge-f4c78b674-bt6zl 1/1 Running 0 3h36m ``` -Congratulations, you have successfully performed mTLS authentication between your local client (kubectl, using the pinniped CLI) and the impersonation proxy inside the cluster. +Congratulations, you have successfully performed mTLS authentication between your local client (kubectl, using the pinniped CLI) +and the impersonation proxy inside the cluster. To verify that your username and groups are visible to Kubernetes, run the `pinniped whoami` command. @@ -333,5 +417,8 @@ pinniped whoami \ --kubeconfig pinniped-kubeconfig.yaml ``` -Now, to verify that the generated kubeconfig `pinniped-kubeconfig.yaml` has the contents of `self-signed-ca-for-kind-testing.pem.b64` as the contained CA bundle for the cluster, -simply cat out both files to compare. \ No newline at end of file +Finally, verify the expected outcome: + +- View the CA embedded in your kubeconfig file: `cat pinniped-kubeconfig.yaml | yq ".clusters[0].cluster.certificate-authority-data"` +- View the CA provided to the impersonation proxy: `kubectl get CredentialIssuer pinniped-concierge-config -o jsonpath="{.status.strategies[1].frontend.impersonationProxyInfo.certificateAuthorityData}"` +- View the CA we stored in our local PEM file: `cat self-signed-ca-for-kind-testing.pem.b64` From e5e8c13f23016d5b3179c373a2295787d8d86ce6 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Mon, 14 Aug 2023 11:59:23 -0400 Subject: [PATCH 4/5] blog: impersonation-proxy spelling, grammar --- .../posts/2021-06-02-first-ldap-release.md | 2 +- ...impersonation-proxy-with-external-certs.md | 51 ++++++++++--------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/site/content/posts/2021-06-02-first-ldap-release.md b/site/content/posts/2021-06-02-first-ldap-release.md index 2f3267ae..ba3eac32 100644 --- a/site/content/posts/2021-06-02-first-ldap-release.md +++ b/site/content/posts/2021-06-02-first-ldap-release.md @@ -118,7 +118,7 @@ We've provided examples of using [OpenLDAP]({{< ref "docs/howto/install-supervis and [JumpCloud]({{< ref "docs/howto/install-supervisor.md" >}}) as LDAP providers. Stay tuned for examples of using Active Directory. -The `pinniped` CLI has also been enhanced to support LDAP authentication. Now when `pinnped get kubectl` sees +The `pinniped` CLI has also been enhanced to support LDAP authentication. Now when `pinniped get kubectl` sees that your cluster's Concierge is configured to use a Supervisor which has an LDAPIdentityProvider, then it will emit the appropriate kubeconfig to enable LDAP logins. When that kubeconfig is used with `kubectl`, the Pinniped plugin will directly prompt the user on the CLI for their LDAP username and password and diff --git a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md index 726efe42..8bfa8b5a 100644 --- a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md +++ b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md @@ -1,5 +1,5 @@ --- -title: "Pinniped v0.25.0: With External Certificate Management for the Impersonation Proxy and more" +title: "Pinniped v0.25.0: With External Certificate Management for the Impersonation Proxy and more!" slug: v0-25-0-external-cert-mgmt-impersonation-proxy date: 2023-08-09 author: Joshua T. Casey @@ -14,9 +14,9 @@ tags: ['Joshua T. Casey','Ryan Richard', 'Benjamin Petersen', 'release', 'kubern ![Friendly seal](https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80) *Photo by [karlheinz_eckhardt Eckhardt](https://unsplash.com/@karlheinz_eckhardt) on [Unsplash](https://unsplash.com/s/photos/seal)* -With Pinniped v0.25.0 you get the ability to configure an externally-generated certificate for Pinnniped Concierge's impersonation proxy to serve TLS. The +With Pinniped v0.25.0 you get the ability to configure an externally-generated certificate for Pinniped Concierge's impersonation proxy to serve TLS. The impersonation proxy is a component within Pinniped that allows the project to support many types of clusters, such as -[Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/), [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine) +[Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/), [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine), and [Azure Kubernetes Service (AKS)](https://azure.microsoft.com/en-us/overview/kubernetes-on-azure). To read more on this feature, and the design decisions behind it, see the [proposal](https://github.com/vmware-tanzu/pinniped/tree/main/proposals/1547_impersonation-proxy-external-certs). @@ -25,7 +25,8 @@ To read more about the impersonation proxy, see the [docs](https://pinniped.dev/ To see the feature in practice on a local kind cluster, follow these instructions. This will perform mTLS between your local client (kubectl and the pinniped CLI) and the impersonation proxy. -The setup: We will be using a kind cluster, Contour as an ingress to the impersonation proxy, and `cert-manager` to generate a TLS serving cert. +The setup: We will be using a kind cluster, Contour as an ingress to the impersonation proxy, and cert-manager to generate a TLS serving cert. +The setup: We will be using a kind cluster, Contour as an ingress to the impersonation proxy, and cert-manager to generate a TLS serving cert. ```shell Docker desktop v1.20.1 @@ -48,8 +49,8 @@ $ kind create cluster \ ``` Now we will install Contour (see https://projectcontour.io/getting-started/ for more details). Contour provides our kind -cluster with an Ingress Controller. We will later deploy a Contour HTTPProxy in order to create DNS that we can -use to access the Impersonation Proxy. +cluster with an Ingress Controller. We will later deploy a Contour HTTPProxy to create DNS that we can +use to access the impersonation proxy. ```shell # From https://projectcontour.io/getting-started/ @@ -63,8 +64,8 @@ $ kubectl get pods \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Pinniped's local-user-authenticator will act as a dummy Identity Provider for our example. This resource is not for production -use, but is sufficient for our needs to exercise the new feature of the impersonation proxy. Install Pinniped’s local-user-authenticator +Pinniped's local-user-authenticator will act as a dummy identity provider for our example. This resource is not for production +use but is sufficient for our needs to exercise the new feature of the impersonation proxy. Install Pinniped’s local-user-authenticator and add some sample users (see https://pinniped.dev/docs/tutorials/concierge-only-demo/ for more details). ```shell @@ -89,7 +90,7 @@ $ kubectl get secret local-user-authenticator-tls-serving-certificate \ ``` In this example, we are only interacting with the Pinniped's Concierge. The Supervisor is not in use as we are not interacting -with a real external OIDC Identity Provider. Install Pinniped's Concierge: +with a real external OIDC identity provider. Install Pinniped's Concierge: ```shell $ kubectl apply \ @@ -101,8 +102,8 @@ $ kubectl apply \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -To handle X.509 certificate management for us, we will install cert-manager. For the purposes of this exercise, we will use `cert-manager` -to generate our CA certificates as well as our TLS serving certificates. Install `cert-manager`: +To handle X.509 certificate management for us, we will install cert-manager. For the purposes of this exercise, we will use cert-manager +to generate our CA certificates as well as our TLS serving certificates. Install cert-manager: ```shell $ kubectl apply \ @@ -110,20 +111,20 @@ $ kubectl apply \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -For this demonstration we will be using `cert-manager` to simulate our own Public Key Infrastructure (PKI). +For this demonstration, we will be using cert-manager to simulate our own Public Key Infrastructure (PKI). We will create the appropriate CA certificates and TLS serving certificates for the impersonation proxy to serve TLS. -For more information about using `cert-manager` to achieve this, see the [cert-manager docs](https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers). +For more information about using cert-manager to achieve this, see the [cert-manager docs](https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers). In summary, we will do the following: - Create two `ClusterIssuer` resources, one named `selfsigned-cluster-issuer` and another named `my-ca-issuer`. -- The `ClusterIssuer` named `my-ca-issuer` will be used to create several `Certificat`e resources. First, we will create +- The `ClusterIssuer` named `my-ca-issuer` will be used to create several `Certificate` resources. First, we will create the `Certificate` called `my-selfsigned-ca` (which will reference a `Secret` named `self-signed-ca-for-kind-testing` where the actual certificate data will be stored). - We will later retrieve the `Secret` called `self-signed-ca-for-kind-testing` so that we can add the CA to the Pinniped Concierge's `CredentialIssuer` resource so that it can be advertised and used to verify TLS serving certificates. - Then, we will create the `ClusterIssuer` called `my-ca-issuer`. We will reference the `Certificate` called `my-selfsigned-ca` via - it's `Secret` named `self-signed-ca-for-kind-testing`. This will allow us to use the CA to sign TLS serving certificates. + its `Secret` named `self-signed-ca-for-kind-testing`. This will allow us to use the CA to sign TLS serving certificates. - Then, we will use the `ClusterIssuer` called `my-ca-issuer` to generate a `Certificate` that will be a TLS serving certificate called `impersonation-serving-cert`. As before, the actual certificate data will be stored in a Kubernetes `Secret` which we will name `impersonation-proxy-tls-serving-cert`. @@ -221,7 +222,7 @@ $ kubectl get secret self-signed-ca-for-kind-testing \ ``` The `CredentialIssuer` resource called `pinniped-concierge-config` already exists. We need to edit it. -Kind clusters have no need to use the impersonation proxy by default (it is designed for public cloud providers), +Kind clusters do not need to use the impersonation proxy by default (it is designed for public cloud providers), so we will make several changes to this resource: - Set the `spec.impersonationProxy.mode: enabled` @@ -294,14 +295,14 @@ $ kubectl get service pinniped-concierge-impersonation-proxy-cluster-ip \ --kubeconfig kind-with-contour.kubeconfig.yaml ``` -Configure a webhook authenticator to tell Concierge to validate static tokens using the installed `local-user-authenticator`. -When we installed the Pinniped `local-user-authenticator`, we created a service called `local-user-authenticator` in the -`local-user-authenticator` namespace. We previously retrieved the Secret named `local-user-authenticator-tls-serving-certificate` +Configure a webhook authenticator to tell Concierge to validate static tokens using the installed local-user-authenticator. +When we installed the Pinniped local-user-authenticator, we created a service called local-user-authenticator in the +local-user-authenticator namespace. We previously retrieved the Secret named `local-user-authenticator-tls-serving-certificate` so that we could use it to configure this `WebhookAuthenticator` to use that certificate. Note that we did not generate this -certificate via `cert-manager`, this is still a self-signed certificate created by Pinniped. +certificate via cert-manager, this is still a self-signed certificate created by Pinniped. The `endpoint` here is referenced via Kubernetes DNS in the format `..svc` targeting the `/authenticate` -endpoint of the `local-user-authenticator`. We will be using https, if course. +endpoint of the local-user-authenticator. We will be using https, if course. ```yaml # Configure a webhook authenticator to tell Concierge to validate static tokens using the installed local-user-authenticator @@ -330,7 +331,7 @@ with the impersonation proxy, and so that client certs used for mTLS will be sen Note in particular the `spec.tcpproxy` block, which is different than the typical `spec.rules` block. `spec.tcpproxy` is required when using `spec.virtualhost.tls.passthrough: true`. -See https://projectcontour.io/docs/1.25/config/tls-termination/#tls-session-passthrough for more details. +See [contour docs for tls session passthrough](https://projectcontour.io/docs/1.25/config/tls-termination/#tls-session-passthrough) for more details. ```shell $ cat << EOF > contour-ingress-impersonation-proxy.yaml @@ -367,10 +368,10 @@ In this example, we will edit the `/etc/hosts` file to resolve the `impersonatio 127.0.0.1 impersonation-proxy-mtls.local ``` -Note that using a static-token does embed those credentials into your kubeconfig. This is not suitable for a production -deployment. As we said before, we are using `local-user-authenticator` as a simple Identity Provider for illustrative purposes +Note that using a static-token does embed those credentials into your kubeconfig. This is not suitable for production +deployment. As we said before, we are using local-user-authenticator as a simple identity provider for illustrative purposes only. In a real production use case you would not employ the `--static-token` flag which would ensure credentials are not -embedded in your kubeconfig, an important security feature. Never use `local-user-authenticator` in production. +embedded in your kubeconfig, an important security feature. Never use local-user-authenticator in production. ```shell # be sure you added 127.0.0.1 impersonation-proxy-mtls.local to your /etc/hosts! From 820c565d21c538492e47d2c64f72f39527ac27e7 Mon Sep 17 00:00:00 2001 From: "Benjamin A. Petersen" Date: Mon, 14 Aug 2023 13:07:04 -0400 Subject: [PATCH 5/5] blog: add multiple author support for posts --- ...v0.25.0-impersonation-proxy-with-external-certs.md | 2 +- site/themes/pinniped/layouts/_default/single.html | 4 +--- site/themes/pinniped/layouts/partials/authors.html | 11 +++++++++++ .../pinniped/layouts/partials/blog-post-card.html | 1 - 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 site/themes/pinniped/layouts/partials/authors.html diff --git a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md index 8bfa8b5a..a4ebbc12 100644 --- a/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md +++ b/site/content/posts/2023-08-09-v0.25.0-impersonation-proxy-with-external-certs.md @@ -8,7 +8,7 @@ authors: - Benjamin A. Petersen image: https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80 excerpt: "With v0.25.0 you get external certificate management for the impersonation proxy, easier scheduling of the kube-cert-agent, and more" -tags: ['Joshua T. Casey','Ryan Richard', 'Benjamin Petersen', 'release', 'kubernetes', 'pki', 'pinniped', 'tls', 'mtls', 'kind', 'contour', 'cert-manager'] +tags: ['Joshua T. Casey','Ryan Richard', 'Benjamin A. Petersen', 'release', 'kubernetes', 'pki', 'pinniped', 'tls', 'mtls', 'kind', 'contour', 'cert-manager'] --- ![Friendly seal](https://images.unsplash.com/photo-1618075254460-429d47b887c7?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2148&q=80) diff --git a/site/themes/pinniped/layouts/_default/single.html b/site/themes/pinniped/layouts/_default/single.html index 680aab6c..53a2fb80 100644 --- a/site/themes/pinniped/layouts/_default/single.html +++ b/site/themes/pinniped/layouts/_default/single.html @@ -9,9 +9,7 @@

{{ .Title }}

-

- {{ .Params.author }} -

+ {{ partial "authors" .}}

{{ dateFormat "Jan 2, 2006" .Date }}

{{ .Content }}
diff --git a/site/themes/pinniped/layouts/partials/authors.html b/site/themes/pinniped/layouts/partials/authors.html new file mode 100644 index 00000000..232f1c32 --- /dev/null +++ b/site/themes/pinniped/layouts/partials/authors.html @@ -0,0 +1,11 @@ +

+ {{ if (isset .Params "authors") }} + {{ $authsCount := .Params.authors | len }} + {{ $commaCount := sub $authsCount 2 }} + {{ range $i, $author := .Params.authors -}} + {{ $author }}{{ if le $i $commaCount }},{{ end }} + {{ end }} + {{ else }} + {{ .Params.author }} + {{ end }} +

diff --git a/site/themes/pinniped/layouts/partials/blog-post-card.html b/site/themes/pinniped/layouts/partials/blog-post-card.html index eed82844..6f7b3c27 100644 --- a/site/themes/pinniped/layouts/partials/blog-post-card.html +++ b/site/themes/pinniped/layouts/partials/blog-post-card.html @@ -5,7 +5,6 @@

{{ .Title }}

-

{{ .Params.author }}

{{ .Params.Excerpt }}