diff --git a/README.md b/README.md index b4c27431..196569d0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ install procedure across all types and origins of Kubernetes clusters, declarative configuration via Kubernetes APIs, enterprise-grade integrations with IDPs, and distribution-specific integration strategies. -### Example Use Cases +### Example use cases * Your team uses a large enterprise IDP, and has many clusters that they manage. Pinniped provides: @@ -39,17 +39,15 @@ The Pinniped Concierge can be configured to hook into the Pinniped Supervisor's federated credentials, or it can authenticate users directly via external IDP credentials. -To learn more, see [architecture](https://pinniped.dev/docs/architecture/). +To learn more, see [architecture](https://pinniped.dev/docs/background/architecture/). -Pinniped Architecture Sketch - -## Trying Pinniped +## Getting started with Pinniped Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/demo/). -## Community Meetings +## Community meetings -Pinniped is better because of our contributors and maintainers. It is because of you that we can bring great software to the community. Please join us during our online community meetings, occuring every first and third Thursday of the month at 9AM PT / 12PM PT. Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09) to attend and add any agenda items you wish to discuss to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view). Join our [Google Group](https://groups.google.com/u/1/g/project-pinniped) to receive invites to this meeting. +Pinniped is better because of our contributors and maintainers. It is because of you that we can bring great software to the community. Please join us during our online community meetings, occurring every first and third Thursday of the month at 9 AM PT / 12 PM PT. Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09) to attend and add any agenda items you wish to discuss to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view). Join our [Google Group](https://groups.google.com/u/1/g/project-pinniped) to receive invites to this meeting. If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled. @@ -61,7 +59,7 @@ Got a question, comment, or idea? Please don't hesitate to reach out via the Git Contributions are welcome. Before contributing, please see the [contributing guide](CONTRIBUTING.md). -## Reporting Security Vulnerabilities +## Reporting security vulnerabilities Please follow the procedure described in [SECURITY.md](SECURITY.md). diff --git a/deploy/concierge/README.md b/deploy/concierge/README.md index bd65216f..39ce06b2 100644 --- a/deploy/concierge/README.md +++ b/deploy/concierge/README.md @@ -1,39 +1,3 @@ -# Deploying +# Pinniped Concierge Deployment -## Connecting Pinniped to an Identity Provider - -If you would like to try Pinniped, but you don't have a compatible identity provider, -you can use Pinniped's test identity provider. -See [deploy/local-user-authenticator/README.md](../../deploy/local-user-authenticator/README.md) -for details. - -## Installing the Latest Version with Default Options - -```bash -kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml -``` - -## Installing a Specific Version with Default Options - -Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number -and use it to replace the version number in the URL below. - -```bash -# Replace v0.4.1 with your preferred version in the URL below -kubectl apply -f https://get.pinniped.dev/v0.4.1/install-pinniped-concierge.yaml -``` - -## Installing with Custom Options - -Creating your own deployment YAML file requires `ytt` from [Carvel](https://carvel.dev/) to template the YAML files -in the `deploy/concierge` directory. -Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags). - -1. `git clone` this repo and `git checkout` the release version tag of the release that you would like to deploy. -1. The configuration options are in [deploy/concierge/values.yml](values.yaml). - Fill in the values in that file, or override those values using additional `ytt` command-line options in - the command below. Use the release version tag as the `image_tag` value. -2. In a terminal, cd to this `deploy/concierge` directory -3. To generate the final YAML files, run `ytt --file .` -4. Deploy the generated YAML using your preferred deployment tool, such as `kubectl` or [`kapp`](https://get-kapp.io/). - For example: `ytt --file . | kapp deploy --yes --app pinniped --diff-changes --file -` +See [the how-to guide for details](https://pinniped.dev/docs/howto/install-concierge/). diff --git a/deploy/supervisor/README.md b/deploy/supervisor/README.md index 4df8f81c..04d84b81 100644 --- a/deploy/supervisor/README.md +++ b/deploy/supervisor/README.md @@ -1,184 +1,3 @@ -# Deploying the Pinniped Supervisor +# Pinniped Supervisor Deployment -## What is the Pinniped Supervisor? - -The Pinniped Supervisor app is a component of the Pinniped OIDC and Cluster Federation solutions. -It can be deployed when those features are needed. - -## Installing the Latest Version with Default Options - -```bash -kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-supervisor.yaml -``` - -## Installing a Specific Version with Default Options - -Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number -and use it to replace the version number in the URL below. - -```bash -# Replace v0.4.1 with your preferred version in the URL below -kubectl apply -f https://get.pinniped.dev/v0.4.1/install-pinniped-supervisor.yaml -``` - -## Installing with Custom Options - -Creating your own deployment YAML file requires `ytt` from [Carvel](https://carvel.dev/) to template the YAML files -in the `deploy/supervisor` directory. -Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags). - -1. `git clone` this repo and `git checkout` the release version tag of the release that you would like to deploy. -1. The configuration options are in [deploy/supervisor/values.yml](values.yaml). - Fill in the values in that file, or override those values using additional `ytt` command-line options in - the command below. Use the release version tag as the `image_tag` value. -2. In a terminal, cd to this `deploy/supervisor` directory -3. To generate the final YAML files, run `ytt --file .` -4. Deploy the generated YAML using your preferred deployment tool, such as `kubectl` or [`kapp`](https://get-kapp.io/). - For example: `ytt --file . | kapp deploy --yes --app pinniped-supervisor --diff-changes --file -` - -## Configuring After Installing - -### Exposing the Supervisor App as a Service - -The Supervisor app's endpoints should be exposed as HTTPS endpoints with proper TLS certificates signed by a -Certificate Authority which will be trusted by your user's web browsers. Because there are -many ways to expose TLS services from a Kubernetes cluster, the Supervisor app leaves this up to the user. -The most common ways are: - -1. Define an [`Ingress` resource](https://kubernetes.io/docs/concepts/services-networking/ingress/) with TLS certificates. - In this case, the ingress will terminate TLS. Typically, the ingress will then talk plain HTTP to its backend, - which would be a NodePort or LoadBalancer Service in front of the HTTP port 8080 of the Supervisor pods. - - The required configuration of the Ingress is specific to your cluster's Ingress Controller, so please refer to the - documentation from your Kubernetes provider. If you are using a cluster from a cloud provider, then you'll probably - want to start with that provider's documentation. For example, if your cluster is a Google GKE cluster, refer to - the [GKE documentation for Ingress](https://cloud.google.com/kubernetes-engine/docs/concepts/ingress). - Otherwise, the Kubernetes documentation provides a list of popular - [Ingress Controllers](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/), including - [Contour](https://projectcontour.io/) and many others. - -1. Or, define a [TCP LoadBalancer Service](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) - which is a layer 4 load balancer and does not terminate TLS. In this case, the Supervisor app will need to be - configured with TLS certificates and will terminate the TLS connection itself (see the section about FederationDomain - below). The LoadBalancer Service should be configured to use the HTTPS port 443 of the Supervisor pods as its `targetPort`. - - *Warning:* Do not expose the Supervisor's port 8080 to the public. It would not be secure for the OIDC protocol - to use HTTP, because the user's secret OIDC tokens would be transmitted across the network without encryption. - -1. Or, expose the Supervisor app using a Kubernetes service mesh technology, e.g. [Istio](https://istio.io/). - Please see the documentation for your service mesh. Generally, the setup would be similar to the description - above for defining an ingress, expect the service mesh would probably provide both the ingress with TLS termination - and the service. - -For either of the first two options mentioned above, if you installed using `ytt` then you can use -the related `service_*` options from [deploy/supervisor/values.yml](values.yaml) to create a Service. -If you installed using `install-supervisor.yaml` then you can create -the Service separately after installing the Supervisor app. There is no `Ingress` included in the `ytt` templates, -so if you choose to use an Ingress then you'll need to create that separately after installing the Supervisor app. - -#### Example: Using a LoadBalancer Service - -This is an example of creating a LoadBalancer Service to expose port 8443 of the Supervisor app outside the cluster. - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: pinniped-supervisor-loadbalancer - # Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml. - namespace: pinniped-supervisor -spec: - type: LoadBalancer - selector: - # Assuming that this is how the supervisor pods are labeled. This is the default in install-supervisor.yaml. - app: pinniped-supervisor - ports: - - protocol: TCP - port: 443 - targetPort: 8443 -``` - -#### Example: Using a NodePort Service - -A NodePort Service exposes the app as a port on the nodes of the cluster. - -This is convenient for use with kind clusters, because kind can -[expose node ports as localhost ports on the host machine](https://kind.sigs.k8s.io/docs/user/configuration/#extra-port-mappings) -without requiring an Ingress, although -[kind also supports several Ingress Controllers](https://kind.sigs.k8s.io/docs/user/ingress). - -A NodePort Service could also be used behind an Ingress which is terminating TLS. - -For example: - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: pinniped-supervisor-nodeport - # Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml. - namespace: pinniped-supervisor -spec: - type: NodePort - selector: - # Assuming that this is how the supervisor pods are labeled. This is the default in install-supervisor.yaml. - app: pinniped-supervisor - ports: - - protocol: TCP - port: 80 - targetPort: 8080 - nodePort: 31234 # This is the port that you would forward to the kind host. Or omit this key for a random port. -``` - -### Configuring the Supervisor to Act as an OIDC Provider - -The Supervisor can be configured as an OIDC provider by creating `FederationDomain` resources -in the same namespace where the Supervisor app was installed. For example: - -```yaml -apiVersion: config.supervisor.pinniped.dev/v1alpha1 -kind: FederationDomain -metadata: - name: my-provider - # Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml. - namespace: pinniped-supervisor -spec: - # The hostname would typically match the DNS name of the public ingress or load balancer for the cluster. - # Any path can be specified, which allows a single hostname to have multiple different issuers. The path is optional. - issuer: https://my-issuer.example.com/any/path - - # Optionally configure the name of a Secret in the same namespace, of type `kubernetes.io/tls`, - # which contains the TLS serving certificate for the HTTPS endpoints served by this OIDC Provider. - tls: - secretName: my-tls-cert-secret -``` - -#### Configuring TLS for the Supervisor OIDC Endpoints - -If you have terminated TLS outside the app, for example using an Ingress with TLS certificates, then you do not need to -configure TLS certificates on the FederationDomain. - -If you are using a LoadBalancer Service to expose the Supervisor app outside your cluster, then you will -also need to configure the Supervisor app to terminate TLS. There are two places to configure TLS certificates: - -1. Each `FederationDomain` can be configured with TLS certificates, using the `spec.tls.secretName` field. - -1. The default TLS certificate for all OIDC providers can be configured by creating a Secret called -`pinniped-supervisor-default-tls-certificate` in the same namespace in which the Supervisor was installed. - -The default TLS certificate will be used for all OIDC providers which did not declare a `spec.tls.secretName`. -Also, the `spec.tls.secretName` will be ignored for incoming requests to the OIDC endpoints -that use an IP address as the host, so those requests will always present the default TLS certificates -to the client. When the request includes the hostname, and that hostname matches the hostname of an `Issuer`, -then the TLS certificate defined by the `spec.tls.secretName` will be used. If that issuer did not -define `spec.tls.secretName` then the default TLS certificate will be used. If neither exists, -then the client will get a TLS error because the server will not present any TLS certificate. - -It is recommended that you have a DNS entry for your load balancer or Ingress, and that you configure the -OIDC provider's `Issuer` using that DNS hostname, and that the TLS certificate for that provider also -covers that same hostname. - -You can create the certificate Secrets however you like, for example you could use [cert-manager](https://cert-manager.io/) -or `kubectl create secret tls`. -Keep in mind that your users will load some of these endpoints in their web browsers, so the TLS certificates -should be signed by a Certificate Authority that will be trusted by their browsers. +See [the how-to guide for details](https://pinniped.dev/docs/howto/install-supervisor/). diff --git a/site/content/community/_index.html b/site/content/community/_index.html index 5726b819..d4c9aa60 100644 --- a/site/content/community/_index.html +++ b/site/content/community/_index.html @@ -16,8 +16,8 @@ layout: section }}">
-

}}">Check out Github

-

Head over to our git repo and check out the discussions and issues sections.

+

}}">Check out GitHub

+

Head over to our GitHub repo and check out the discussions and issues.

diff --git a/site/content/docs/_index.md b/site/content/docs/_index.md index 4eef0121..704c0d52 100644 --- a/site/content/docs/_index.md +++ b/site/content/docs/_index.md @@ -1,5 +1,4 @@ --- -title: "Pinniped Documentation" cascade: layout: docs menu: @@ -8,58 +7,25 @@ menu: weight: 1 --- -![Pinniped Logo](/docs/img/pinniped_logo.svg) +# Getting started with Pinniped -## Overview +Pinniped is an authentication service for Kubernetes clusters. +As a Kubernetes cluster administrator or user, you can learn how Pinniped works, see how to use it on your clusters, and dive into internals of Pinniped's APIs and architecture. -Pinniped provides identity services to Kubernetes. +Have a question, comment, or idea? Please reach out via [GitHub Discussions](https://github.com/vmware-tanzu/pinniped/discussions) or [join the Pinniped community meetings]({{< ref "/community" >}}). -Pinniped allows cluster administrators to easily plug in external identity -providers (IDPs) into Kubernetes clusters. This is achieved via a uniform -install procedure across all types and origins of Kubernetes clusters, -declarative configuration via Kubernetes APIs, enterprise-grade integrations -with IDPs, and distribution-specific integration strategies. +## Tutorials -### Example Use Cases +{{< docsmenu "tutorials" >}} -* Your team uses a large enterprise IDP, and has many clusters that they - manage. Pinniped provides: - * Seamless and robust integration with the IDP - * Easy installation across clusters of any type and origin - * A simplified login flow across all clusters -* Your team shares a single cluster. Pinniped provides: - * Simple configuration to integrate an IDP - * Individual, revocable identities +## How-to guides -### Architecture +{{< docsmenu "howtos" >}} -Pinniped offers credential exchange to enable a user to exchange an external IDP -credential for a short-lived, cluster-specific credential. Pinniped supports various -IDP types and implements different integration strategies for various Kubernetes -distributions to make authentication possible. +## Reference -To learn more, see [docs/architecture](/docs/architecture). +{{< docsmenu "reference" >}} -Pinniped Architecture Sketch +## Background -## Trying Pinniped - -Care to kick the tires? It's easy to [install and try Pinniped](/docs/demo). - -## Discussion - -Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page. - -## Contributions - -Contributions are welcome. Before contributing, please see the [contributing guide](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md). - -## Reporting Security Vulnerabilities - -Please follow the procedure described in [SECURITY.md](https://github.com/vmware-tanzu/pinniped/blob/main/SECURITY.md). - -## License - -Pinniped is open source and licensed under Apache License Version 2.0. See [LICENSE](https://github.com/vmware-tanzu/pinniped/blob/main/LICENSE). - -Copyright 2020 the Pinniped contributors. All Rights Reserved. +{{< docsmenu "background" >}} diff --git a/site/content/docs/background/_index.md b/site/content/docs/background/_index.md new file mode 100644 index 00000000..5916ea0e --- /dev/null +++ b/site/content/docs/background/_index.md @@ -0,0 +1,13 @@ +--- +cascade: + layout: docs +menu: + docs: + name: Background + identifier: background + weight: 80 +--- + +# Pinniped background + +{{< docsmenu "background" >}} diff --git a/site/content/docs/architecture.md b/site/content/docs/background/architecture.md similarity index 96% rename from site/content/docs/architecture.md rename to site/content/docs/background/architecture.md index e6c8acd5..5e6469a2 100644 --- a/site/content/docs/architecture.md +++ b/site/content/docs/background/architecture.md @@ -1,20 +1,20 @@ --- -title: "Pinniped Architecture" +title: Architecture +description: Dive into the overall design and implementation details of Pinniped. cascade: layout: docs menu: docs: name: Architecture weight: 100 + parent: background --- - -# Architecture - The principal purpose of Pinniped is to allow users to access Kubernetes clusters. Pinniped hopes to enable this access across a wide range of Kubernetes environments with zero configuration. Pinniped is composed of two parts. + 1. The Pinniped Supervisor is an OIDC server which allows users to authenticate with an external identity provider (IDP), and then issues its own federation ID tokens to be passed on to clusters based on the user information from the IDP. @@ -28,14 +28,6 @@ understood by the host Kubernetes cluster. Pinniped supports various authenticator types and OIDC identity providers and implements different integration strategies for various Kubernetes distributions to make authentication possible. -## Supported Kubernetes Cluster Types - -Pinniped supports the following types of Kubernetes clusters: - -- Clusters where the Kube Controller Manager pod is accessible from Pinniped's pods. - -Support for other types of Kubernetes distributions is coming soon. - ## External Identity Provider Integrations The Pinniped Supervisor will federate identity from one or more IDPs. diff --git a/site/content/docs/demo.md b/site/content/docs/demo.md deleted file mode 100644 index 1134f014..00000000 --- a/site/content/docs/demo.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: "Pinniped Demo" -cascade: - layout: docs -menu: - docs: - name: Demo - identifier: demo - weight: 40 ---- - -# Trying Pinniped -1. [Concierge with webhook demo](/docs/concierge-only-demo) -1. [Concierge with Supervisor and JWT authenticator demo](/docs/concierge-and-supervisor-demo) diff --git a/site/content/docs/howto/_index.md b/site/content/docs/howto/_index.md new file mode 100644 index 00000000..d53105e6 --- /dev/null +++ b/site/content/docs/howto/_index.md @@ -0,0 +1,14 @@ +--- +title: Pinniped How-To Guides +cascade: + layout: docs +menu: + docs: + name: How-to Guides + identifier: howtos + weight: 50 +--- + +These how-to guides show you how to install and configure the Pinniped command-line tool, Concierge, and Supervisor: + +{{< docsmenu "howtos" >}} diff --git a/site/content/docs/howto/configure-concierge-jwt.md b/site/content/docs/howto/configure-concierge-jwt.md new file mode 100644 index 00000000..36386de4 --- /dev/null +++ b/site/content/docs/howto/configure-concierge-jwt.md @@ -0,0 +1,142 @@ +--- +title: Configure the Pinniped Concierge to validate JWT tokens +description: Set up JSON Web Token (JWT) based token authentication on an individual Kubernetes cluster. +cascade: + layout: docs +menu: + docs: + name: Configure Concierge JWT Authentication + weight: 25 + parent: howtos +--- +The Concierge can validate [JSON Web Tokens (JWTs)](https://tools.ietf.org/html/rfc7519), which are commonly issued by [OpenID Connect (OIDC)](https://openid.net/connect/) identity providers. + +This guide shows you how to use this capability _without_ the Pinniped Supervisor. +This is most useful if you have only a single cluster and want to authenticate to it via an existing OIDC provider. + +If you have multiple clusters, you may want to [install]({{< ref "install-supervisor" >}}) and [configure]({{< ref "configure-supervisor" >}}) the Pinniped Supervisor. + +## Prerequisites + +Before starting, you should have the [command-line tool installed]({{< ref "install-cli" >}}) locally and [Concierge running in your cluster]({{< ref "install-concierge" >}}). + +You should also have some existing OIDC issuer configuration: + +- An OIDC provider that supports [discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) and the `email` scope. +- A public client with callback URI `http://127.0.0.1:12345/callback` and `email` scope. + +## Create a JWTAuthenticator + +Create a JWTAuthenticator describing how to validate tokens from your OIDC issuer: + +```yaml +apiVersion: authentication.concierge.pinniped.dev/v1alpha1 +kind: JWTAuthenticator +metadata: + name: my-jwt-authenticator +spec: + issuer: https://my-issuer.example.com/any/path + audience: my-client-id + claims: + username: email +``` + +If you've saved this into a file `my-jwt-authenticator.yaml`, then install it into your cluster using: + +```sh +kubectl apply -f my-jwt-authenticator.yaml +``` + +## Generate a kubeconfig file + +Generate a kubeconfig file to target the JWTAuthenticator: + +```sh +pinniped get kubeconfig \ + --oidc-client-id my-client-id \ + --oidc-scopes openid,email \ + --oidc-listen-port 12345 \ + > my-cluster.yaml +``` + +This creates a kubeconfig YAML file `my-cluster.yaml` that targets your JWTAuthenticator using `pinniped login oidc` as an [ExecCredential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins). + +It should look something like below: + +```yaml +apiVersion: v1 +kind: Config +current-context: pinniped +clusters: +- cluster: + certificate-authority-data: LS0tLS[...] + server: https://my-kubernetes-api-endpoint.example.com:59986 + name: pinniped +contexts: +- context: + cluster: pinniped + user: pinniped + name: pinniped +users: +- name: pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + command: /usr/local/bin/pinniped + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=my-jwt-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://my-kubernetes-api-endpoint.example.com:59986 + - --concierge-ca-bundle-data=LS0tLS[...] + - --issuer=https://my-oidc-issuer.example.com/any/path + - --client-id=my-client-id + - --scopes=offline_access,openid,email + - --listen-port=12345 + - --request-audience=my-client-id +``` + +## Use the kubeconfig file + +Use the kubeconfig with `kubectl` to access your cluster: + +```sh +kubectl --kubeconfig my-cluster.yaml get namespaces +``` + +You should see: + +- The `pinniped login oidc` command is executed automatically by `kubectl`. + +- Pinniped opens your browser window and directs you to login with your identity provider. + +- After you've logged in, you see a page telling you `you have been logged in and may now close this tab`. + +- In your shell, you see your clusters namespaces. + + If instead you get an access denied error, you may need to create a ClusterRoleBinding for the `email` of your OIDC account, for example: + + ```sh + kubectl create clusterrolebinding my-user-admin \ + --clusterrole admin \ + --user my-username@example.com + ``` + +## Other notes + +- Pinniped kubeconfig files do not contain secrets and are safe to share between users. + +- Temporary OIDC session credentials such as ID, access, and refresh tokens are stored in: + - `~/.config/pinniped/sessions.yaml` (macOS/Linux) + - `%USERPROFILE%/.config/pinniped/sessions.yaml` (Windows). + +- If your OIDC provider supports [wildcard port number matching](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-16#section-2.1) for localhost URIs, you can omit the `--oidc-listen-port` flag to use a randomly chosen ephemeral TCP port. + +- The Pinniped command-line tool can only act as a public client with no client secret. + If your provider only supports non-public clients, consider using the Pinniped Supervisor. + +- In general, it is not safe to use the same OIDC client across multiple clusters. + If you need to access multiple clusters, please [install the Pinniped Supervisor]({{< ref "install-supervisor" >}}). \ No newline at end of file diff --git a/site/content/docs/howto/configure-concierge-webhook.md b/site/content/docs/howto/configure-concierge-webhook.md new file mode 100644 index 00000000..ead557fc --- /dev/null +++ b/site/content/docs/howto/configure-concierge-webhook.md @@ -0,0 +1,116 @@ +--- +title: Configure the Pinniped Concierge to validate webhook tokens +description: Set up webhook-based token authentication on an individual Kubernetes cluster. +cascade: + layout: docs +menu: + docs: + name: Configure Concierge Webhook Authentication + weight: 26 + parent: howtos +--- + +The Concierge can validate arbitrary tokens via an external webhook endpoint using the [same validation process as Kubernetes itself](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication). + +## Prerequisites + +Before starting, you should have the [command-line tool installed]({{< ref "install-cli" >}}) locally and [Concierge running in your cluster]({{< ref "install-concierge" >}}). + +You should also have a custom TokenReview webhook endpoint: + +- Your webhook endpoint must handle the `authentication.k8s.io/v1` [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/#TokenReview). + +- Your webhook must be accessible from the Concierge pod over HTTPS. + +## Create a WebhookAuthenticator + +Create a WebhookAuthenticator describing how to validate tokens using your webhook: + +```yaml +apiVersion: authentication.concierge.pinniped.dev/v1alpha1 +kind: WebhookAuthenticator +metadata: + name: my-webhook-authenticator +spec: + # HTTPS endpoint to be called as a webhook + endpoint: https://my-webhook.example.com/any/path + tls: + # base64-encoded PEM CA bundle (optional) + certificateAuthorityData: "LS0tLS1CRUdJTi[...]" +``` + +If you've saved this into a file `my-webhook-authenticator.yaml`, then install it into your cluster using: + +```sh +kubectl apply -f my-webhook-authenticator.yaml +``` + +## Generate a kubeconfig file + +Generate a kubeconfig file to target the WebhookAuthenticator: + +```sh +pinniped get kubeconfig \ + --static-token-env MY_CLUSTER_ACCESS_TOKEN \ + > my-cluster.yaml +``` + +This creates a kubeconfig YAML file `my-cluster.yaml` that targets your WebhookAuthenticator using `pinniped login static` as an [ExecCredential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins). + +It should look something like below: + +```yaml +apiVersion: v1 +kind: Config +current-context: pinniped +clusters: +- cluster: + certificate-authority-data: LS0tLS[...] + server: https://my-kubernetes-api-endpoint.example.com:59986 + name: pinniped +contexts: +- context: + cluster: pinniped + user: pinniped + name: pinniped +users: +- name: pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + command: /usr/local/bin/pinniped + args: + - login + - oidc + - login + - static + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=my-webhook-authenticator + - --concierge-authenticator-type=webhook + - --concierge-endpoint=https://127.0.0.1:59986 + - --concierge-ca-bundle-data=LS0tLS[...] + - --token-env=MY_CLUSTER_ACCESS_TOKEN +``` + +## Use the kubeconfig file + +Set the `$MY_CLUSTER_ACCESS_TOKEN` environment variable and use the kubeconfig with `kubectl` to access your cluster: + +```sh +MY_CLUSTER_ACCESS_TOKEN=secret-token kubectl --kubeconfig my-cluster.yaml get namespaces +``` + +You should see: + +- The `pinniped login static` command is silently executed automatically by `kubectl`. + +- The command-line tool sends your token to the Concierge which validates it by making a request to your webhook endpoint. + +- In your shell, you see your clusters namespaces. + + If instead you get an access denied error, you may need to create a ClusterRoleBinding for the username/groups returned by your webhook, for example: + + ```sh + kubectl create clusterrolebinding my-user-admin --clusterrole admin --user my-username + ``` diff --git a/site/content/docs/howto/configure-supervisor.md b/site/content/docs/howto/configure-supervisor.md new file mode 100644 index 00000000..a5ddf342 --- /dev/null +++ b/site/content/docs/howto/configure-supervisor.md @@ -0,0 +1,161 @@ +--- +title: Configure the Pinniped Supervisor as an OIDC issuer +description: Set up the Pinniped Supervisor to provide seamless login flows across multiple clusters. +cascade: + layout: docs +menu: + docs: + name: Configure Supervisor + weight: 35 + parent: howtos +--- +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single "upstream" OIDC identity provider to many "downstream" cluster clients. + +This guide show you how to use this capability to issue [JSON Web Tokens (JWTs)](https://tools.ietf.org/html/rfc7519) that can be validated by the [Pinniped Concierge]({{< ref "configure-concierge-jwt" >}}). + +Before starting, you should have the [command-line tool installed]({{< ref "install-cli" >}}) locally and the Concierge [installed]({{< ref "install-concierge" >}}) in your cluster. + +## Expose the Supervisor app as a service + +The Supervisor app's endpoints should be exposed as HTTPS endpoints with proper TLS certificates signed by a certificate authority (CA) which is trusted by your user's web browsers. + +Because there are many ways to expose TLS services from a Kubernetes cluster, the Supervisor app leaves this up to the user. +The most common ways are: + +1. Define an [`Ingress` resource](https://kubernetes.io/docs/concepts/services-networking/ingress/) with TLS certificates. + In this case, the ingress terminates TLS. Typically, the ingress then talks plain HTTP to its backend, + which would be a NodePort or LoadBalancer Service in front of the HTTP port 8080 of the Supervisor pods. + + The required configuration of the Ingress is specific to your cluster's Ingress Controller, so please refer to the + documentation from your Kubernetes provider. If you are using a cluster from a cloud provider, then you'll probably + want to start with that provider's documentation. For example, if your cluster is a Google GKE cluster, refer to + the [GKE documentation for Ingress](https://cloud.google.com/kubernetes-engine/docs/concepts/ingress). + Otherwise, the Kubernetes documentation provides a list of popular + [Ingress Controllers](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/), including + [Contour](https://projectcontour.io/) and many others. + +1. Or, define a [TCP LoadBalancer Service](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) + which is a layer 4 load balancer and does not terminate TLS. In this case, the Supervisor app needs to be + configured with TLS certificates and terminates the TLS connection itself (see the section about FederationDomain + below). The LoadBalancer Service should be configured to use the HTTPS port 443 of the Supervisor pods as its `targetPort`. + + *Warning:* do not expose the Supervisor's port 8080 to the public. It would not be secure for the OIDC protocol + to use HTTP, because the user's secret OIDC tokens would be transmitted across the network without encryption. + +1. Or, expose the Supervisor app using a Kubernetes service mesh technology, for example [Istio](https://istio.io/). + Please see the documentation for your service mesh. Generally, the setup would be similar to the previous description + for defining an ingress, except the service mesh would probably provide both the ingress with TLS termination + and the service. + +For either of the first two options, if you installed using `ytt` then you can use +the related `service_*` options from [deploy/supervisor/values.yml](values.yaml) to create a Service. +If you installed using `install-supervisor.yaml` then you can create +the Service separately after installing the Supervisor app. There is no `Ingress` included in the `ytt` templates, +so if you choose to use an Ingress then you'll need to create that separately after installing the Supervisor app. + +#### Example: Using a LoadBalancer Service + +This is an example of creating a LoadBalancer Service to expose port 8443 of the Supervisor app outside the cluster. + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: pinniped-supervisor-loadbalancer + # Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml. + namespace: pinniped-supervisor +spec: + type: LoadBalancer + selector: + # Assuming that this is how the supervisor pods are labeled. This is the default in install-supervisor.yaml. + app: pinniped-supervisor + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +``` + +#### Example: Using a NodePort Service + +A NodePort Service exposes the app as a port on the nodes of the cluster. + +This is convenient for use with kind clusters, because kind can +[expose node ports as localhost ports on the host machine](https://kind.sigs.k8s.io/docs/user/configuration/#extra-port-mappings) +without requiring an Ingress, although +[kind also supports several Ingress Controllers](https://kind.sigs.k8s.io/docs/user/ingress). + +A NodePort Service could also be used behind an Ingress which is terminating TLS. + +For example: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: pinniped-supervisor-nodeport + # Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml. + namespace: pinniped-supervisor +spec: + type: NodePort + selector: + # Assuming that this is how the supervisor pods are labeled. This is the default in install-supervisor.yaml. + app: pinniped-supervisor + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + nodePort: 31234 # This is the port that you would forward to the kind host. Or omit this key for a random port. +``` + +### Configuring the Supervisor to act as an OIDC provider + +The Supervisor can be configured as an OIDC provider by creating `FederationDomain` resources +in the same namespace where the Supervisor app was installed. For example: + +```yaml +apiVersion: config.supervisor.pinniped.dev/v1alpha1 +kind: FederationDomain +metadata: + name: my-provider + # Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml. + namespace: pinniped-supervisor +spec: + # The hostname would typically match the DNS name of the public ingress or load balancer for the cluster. + # Any path can be specified, which allows a single hostname to have multiple different issuers. The path is optional. + issuer: https://my-issuer.example.com/any/path + + # Optionally configure the name of a Secret in the same namespace, of type `kubernetes.io/tls`, + # which contains the TLS serving certificate for the HTTPS endpoints served by this OIDC Provider. + tls: + secretName: my-tls-cert-secret +``` + +#### Configuring TLS for the Supervisor OIDC endpoints + +If you have terminated TLS outside the app, for example using an Ingress with TLS certificates, then you do not need to +configure TLS certificates on the FederationDomain. + +If you are using a LoadBalancer Service to expose the Supervisor app outside your cluster, then you +also need to configure the Supervisor app to terminate TLS. There are two places to configure TLS certificates: + +1. Each `FederationDomain` can be configured with TLS certificates, using the `spec.tls.secretName` field. + +1. The default TLS certificate for all OIDC providers can be configured by creating a Secret called +`pinniped-supervisor-default-tls-certificate` in the same namespace in which the Supervisor was installed. + +The default TLS certificate are used for all OIDC providers which did not declare a `spec.tls.secretName`. +Also, the `spec.tls.secretName` is ignored for incoming requests to the OIDC endpoints +that use an IP address as the host, so those requests always present the default TLS certificates +to the client. When the request includes the hostname, and that hostname matches the hostname of an `Issuer`, +then the TLS certificate defined by the `spec.tls.secretName` is used. If that issuer did not +define `spec.tls.secretName` then the default TLS certificate is used. If neither exists, +then the client gets a TLS error because the server does not present any TLS certificate. + +It is recommended that you have a DNS entry for your load balancer or Ingress, and that you configure the +OIDC provider's `Issuer` using that DNS hostname, and that the TLS certificate for that provider also +covers that same hostname. + +You can create the certificate Secrets however you like, for example you could use [cert-manager](https://cert-manager.io/) +or `kubectl create secret tls`. +Keep in mind that your users must load some of these endpoints in their web browsers, so the TLS certificates +should be signed by a certificate authority that is trusted by their browsers. diff --git a/site/content/docs/howto/install-cli.md b/site/content/docs/howto/install-cli.md new file mode 100644 index 00000000..353d8f43 --- /dev/null +++ b/site/content/docs/howto/install-cli.md @@ -0,0 +1,55 @@ +--- +title: Install the Pinniped command-line tool +description: Download and set up the `pinniped` command-line tool on macOS, Linux, or Windows clients. +cascade: + layout: docs +menu: + docs: + name: Install CLI + weight: 10 + parent: howtos +--- +The `pinniped` command-line tool is used to generate Pinniped-compatible kubeconfig files, and is also an important part of the Pinniped-based login flow. + +It must be installed by administrators setting up a Pinniped cluster as well as by users accessing a Pinniped-enabled cluster. + +## Install using Homebrew on macOS or Linux + +Use [Homebrew](https://brew.sh/) to install from the Pinniped [tap](https://github.com/vmware-tanzu/homebrew-pinniped): + +- `brew install vmware-tanzu/pinniped/pinniped-cli` + +## Download binaries + +Find the appropriate binary for your platform from the [latest release](https://github.com/vmware-tanzu/pinniped/releases/latest): + +{{< buttonlink href="https://get.pinniped.dev/latest/pinniped-cli-darwin-amd64" >}}Download for macOS/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}} + +{{< buttonlink href="https://get.pinniped.dev/latest/pinniped-cli-linux-amd64" >}}Download for Linux/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}} + +{{< buttonlink href="https://get.pinniped.dev/latest/pinniped-cli-windows-amd64" >}}Download for Windows/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}} + +You should put the command-line tool somewhere on your `$PATH`, such as `/usr/local/bin` on macOS/Linux. +You'll also need to mark the file as executable. + +To find specific versions or view all available platforms and architectures, visit the [releases page](https://github.com/vmware-tanzu/pinniped/releases/). + +### Gatekeeper + +If you are using macOS, you may get an error dialog when you first run `pinniped` that says `“pinniped” cannot be opened because the developer cannotbe verified`. +Cancel this dialog, open System Preferences, click Security & Privacy, and click the Allow Anyway button next to the Pinniped message. + +Run the command again and another dialog appears saying `macOS cannot verify the developer of “pinniped”. Are you sure you want to open it?`. +Click Open to allow the command to proceed. + +## Install a specific version via script + +For example, to install v0.4.1 on Linux/amd64: + +```sh +curl -Lso pinniped https://get.pinniped.dev/v0.4.1/pinniped-cli-linux-amd64 \ + && chmod +x pinniped \ + && sudo mv pinniped /usr/local/bin/pinniped +``` + +*Next, [install the Concierge]({{< ref "install-concierge.md" >}})!* diff --git a/site/content/docs/howto/install-concierge.md b/site/content/docs/howto/install-concierge.md new file mode 100644 index 00000000..e8e66714 --- /dev/null +++ b/site/content/docs/howto/install-concierge.md @@ -0,0 +1,60 @@ +--- +title: Install the Pinniped Concierge +description: Install the Pinniped Concierge service in a Kubernetes cluster. +cascade: + layout: docs +menu: + docs: + name: Install Concierge + weight: 20 + parent: howtos +--- +This guide shows you how to install the Pinniped Concierge. +You should have a [supported Kubernetes cluster]({{< ref "../reference/supported-clusters" >}}). + +## With default options + +1. Install the latest version of the Concierge into the `pinniped-concierge` namespace with default options: + + - `kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml` + +## With specific version and default options + +1. Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number and use it to replace the version number in the URL below. + +1. Install the Concierge into the `pinniped-concierge` namespace with default options: + + - `kubectl apply -f https://get.pinniped.dev/v0.4.1/install-pinniped-concierge.yaml` + + *Replace v0.4.1 with your preferred version number.* + +## With custom options + +Pinniped uses [ytt](https://carvel.dev/ytt/) from [Carvel](https://carvel.dev/) as a templating system. + +1. Install the `ytt` command-line tool using the instructions from the [Carvel documentation](https://carvel.dev/#whole-suite). + +1. Clone the Pinniped GitHub repository and visit the `deploy/concierge` directory: + + - `git clone git@github.com:vmware-tanzu/pinniped.git` + - `cd pinniped/deploy/concierge` + +1. Customize configuration parameters: + + - Edit `values.yaml` with your custom values. + - See the [default values](http://github.com/vmware-tanzu/pinniped/tree/main/deploy/concierge/values.yaml) for documentation about individual configuration parameters. + +1. Render templated YAML manifests: + + - `ytt --file .` + +1. Deploy the templated YAML manifests: + + - *If you're using `kubectl`:* + + `ytt --file . | kubectl apply -f -` + - *If you're using [`kapp` from Carvel](https://carvel.dev/kapp/):* + + `ytt --file . | kapp deploy --yes --app pinniped-concierge --diff-changes --file -` + +*Next, configure the Concierge for [JWT]({{< ref "configure-concierge-jwt.md" >}}) or [webhook]({{< ref "configure-concierge-webhook.md" >}}) authentication.* diff --git a/site/content/docs/howto/install-supervisor.md b/site/content/docs/howto/install-supervisor.md new file mode 100644 index 00000000..49267842 --- /dev/null +++ b/site/content/docs/howto/install-supervisor.md @@ -0,0 +1,59 @@ +--- +title: Install the Pinniped Supervisor +description: Install the Pinniped Supervisor service in a Kubernetes cluster. +cascade: + layout: docs +menu: + docs: + name: Install Supervisor + weight: 30 + parent: howtos +--- +This guide shows you how to install the Pinniped Supervisor, which allows seamless login across one or many Kubernetes clusters. +You should have a supported Kubernetes cluster with working HTTPS ingress capabilities. + + +## With default options + +1. Install the latest version of the Supervisor into the `pinniped-supervisor` namespace with default options: + + - `kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-supervisor.yaml` + +## With specific version and default options + +1. Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number and use it to replace the version number in the URL below. + +1. Install theSupervisor into the `pinniped-supervisor` namespace with default options: + + - `kubectl apply -f https://get.pinniped.dev/v0.4.1/install-pinniped-concierge.yaml` + + *Replace v0.4.1 with your preferred version number.* + +## With custom options + +Pinniped uses [ytt](https://carvel.dev/ytt/) from [Carvel](https://carvel.dev/) as a templating system. + +1. Install the `ytt` command-line tool using the instructions from the [Carvel documentation](https://carvel.dev/#whole-suite). + +1. Clone the Pinniped GitHub repository and visit the `deploy/supervisor` directory: + + - `git clone git@github.com:vmware-tanzu/pinniped.git` + - `cd pinniped/deploy/supervisor` + +1. Customize configuration parameters: + + - Edit `values.yaml` with your custom values. + - See the [default values](http://github.com/vmware-tanzu/pinniped/tree/main/deploy/supervisor/values.yaml) for documentation about individual configuration parameters. + +1. Render templated YAML manifests: + + - `ytt --file .` + +1. Deploy the templated YAML manifests: + + - *If you're using `kubectl`:* + + `ytt --file . | kubectl apply -f -` + - *If you're using [`kapp` from Carvel](https://carvel.dev/kapp/):* + + `ytt --file . | kapp deploy --yes --app pinniped-supervisor --diff-changes --file -` diff --git a/site/content/docs/install.md b/site/content/docs/install.md deleted file mode 100644 index 4fffc9ef..00000000 --- a/site/content/docs/install.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: "Installing Pinniped" -cascade: - layout: docs -menu: - docs: - name: Install - weight: 10 ---- - -# Installing Pinniped - -## Install the CLI - -- Find the appropriate binary for your platform from the [latest release](https://github.com/vmware-tanzu/pinniped/releases/latest). - -- Use Homebrew on macOS: `brew install vmware-tanzu/pinniped/pinniped-cli`. - -## Install the Concierge - -- See the [concierge deployment guide](https://github.com/vmware-tanzu/pinniped/tree/main/deploy/concierge). - -## Install the Supervisor - -- See the [supervisor deployment guide](https://github.com/vmware-tanzu/pinniped/tree/main/deploy/supervisor). \ No newline at end of file diff --git a/site/content/docs/reference/_index.md b/site/content/docs/reference/_index.md new file mode 100644 index 00000000..052154c5 --- /dev/null +++ b/site/content/docs/reference/_index.md @@ -0,0 +1,13 @@ +--- +cascade: + layout: docs +menu: + docs: + name: Reference + identifier: reference + weight: 100 +--- + +# Pinniped reference + +{{< docsmenu "reference" >}} diff --git a/site/content/docs/reference/api.md b/site/content/docs/reference/api.md new file mode 100644 index 00000000..95f8fdf2 --- /dev/null +++ b/site/content/docs/reference/api.md @@ -0,0 +1,12 @@ +--- +title: API +description: Reference for the `*.pinniped.dev` Kubernetes API groups. +cascade: + layout: docs +menu: + docs: + name: Kubernetes API + weight: 35 + parent: reference +--- +Full API reference documentation for the Pinniped Kubernetes API is available [on GitHub](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc). diff --git a/site/content/docs/reference/cli.md b/site/content/docs/reference/cli.md new file mode 100644 index 00000000..de1c4e34 --- /dev/null +++ b/site/content/docs/reference/cli.md @@ -0,0 +1,84 @@ +--- +title: Command-Line Options Reference +description: Reference for the `pinniped` command-line tool +cascade: + layout: docs +menu: + docs: + name: Command-Line Options + weight: 30 + parent: reference +--- + +## `pinniped version` + +Print the version of this Pinniped CLI. + +```sh +pinniped version [flags] +``` + +- `-h`, `--help`: + + help for kubeconfig + +## `pinniped get kubeconfig` + +Generate a Pinniped-based kubeconfig for a cluster. + +```sh +pinniped get kubeconfig [flags] +``` + +- `-h`, `--help`: + + help for kubeconfig + +- `--concierge-api-group-suffix string`: + + Concierge API group suffix (default "pinniped.dev") +- `--concierge-authenticator-name string`: + + Concierge authenticator name (default: autodiscover) +- `--concierge-authenticator-type string`: + + Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) +- `--kubeconfig string`: + + Path to kubeconfig file +- `--kubeconfig-context string`: + + Kubeconfig context name (default: current active context) +- `--no-concierge`: + + Generate a configuration which does not use the concierge, but sends the credential to the cluster directly +- `--oidc-ca-bundle strings`: + + Path to TLS certificate authority bundle (PEM format, optional, can be repeated) +- `--oidc-client-id string`: + + OpenID Connect client ID (default: autodiscover) (default "pinniped-cli") +- `--oidc-issuer string`: + + OpenID Connect issuer URL (default: autodiscover) +- `--oidc-listen-port uint16`: + + TCP port for localhost listener (authorization code flow only) +- `--oidc-request-audience string`: + + Request a token with an alternate audience using RFC8693 token exchange +- `--oidc-scopes strings`: + + OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience]) +- `--oidc-session-cache string`: + + Path to OpenID Connect session cache file +- `--oidc-skip-browser`: + + During OpenID Connect login, skip opening the browser (just print the URL) +- `--static-token string`: + + Instead of doing an OIDC-based login, specify a static token +- `--static-token-env string`: + + Instead of doing an OIDC-based login, read a static token from the environment diff --git a/site/content/docs/reference/supported-clusters.md b/site/content/docs/reference/supported-clusters.md new file mode 100644 index 00000000..8ff538f2 --- /dev/null +++ b/site/content/docs/reference/supported-clusters.md @@ -0,0 +1,29 @@ +--- +title: Supported cluster types +description: See the supported cluster types for the Pinniped Concierge. +cascade: + layout: docs +menu: + docs: + name: Supported Cluster Types + weight: 10 + parent: reference +--- + +| **Cluster Type** | **Conciege Works?** | +|-|-| +| [VMware Tanzu Kubernetes Grid (TKG) clusters](https://tanzu.vmware.com/kubernetes-grid) | Yes | +| [Kind clusters](https://kind.sigs.k8s.io/) | Yes | +| [Kubeadm-based clusters](https://kubernetes.io/docs/reference/setup-tools/kubeadm/) | Yes | +| [Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/) | No | +| [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine) | No | +| [Azure Kubernetes Service (AKS)](https://azure.microsoft.com/en-us/overview/kubernetes-on-azure) | No | + +## Background + +The Pinniped Concierge currently supports clusters where a custom pod can be executed on the same node running `kube-controller-manager`. +This type of cluster is typically called "self-hosted" because the cluster's control plane is running on nodes that are part of the cluster itself. + +In practice, this means that many Kubernetes distributions are supported, but not most managed Kubernetes services + +Support for more cluster types, including managed Kubernetes environments, is planned. diff --git a/site/content/docs/tutorials/_index.md b/site/content/docs/tutorials/_index.md new file mode 100644 index 00000000..b95b06cd --- /dev/null +++ b/site/content/docs/tutorials/_index.md @@ -0,0 +1,15 @@ +--- +cascade: + layout: docs +menu: + docs: + name: Tutorials + identifier: tutorials + weight: 40 +--- + +# Pinniped tutorials + +These tutorials demonstrate how to use the Pinniped command-line tool, Concierge, and Supervisor: + +{{< docsmenu "tutorials" >}} diff --git a/site/content/docs/concierge-and-supervisor-demo.md b/site/content/docs/tutorials/concierge-and-supervisor-demo.md similarity index 72% rename from site/content/docs/concierge-and-supervisor-demo.md rename to site/content/docs/tutorials/concierge-and-supervisor-demo.md index ff38d188..382cdfc0 100644 --- a/site/content/docs/concierge-and-supervisor-demo.md +++ b/site/content/docs/tutorials/concierge-and-supervisor-demo.md @@ -1,15 +1,14 @@ --- -title: "Pinniped Concierge and Supervisor Demo" +title: Learn to use the Pinniped Supervisor alongside the Concierge +description: See how the Pinniped Supervisor streamlines login to multiple Kubernetes clusters. cascade: layout: docs menu: docs: name: Concierge with Supervisor - parent: demo + parent: tutorials --- -# Trying Pinniped Supervisor and Concierge - ## Prerequisites 1. A Kubernetes cluster of a type supported by Pinniped Concierge as described in [architecture](/docs/architecture). @@ -17,18 +16,18 @@ menu: Don't have a cluster handy? Consider using [kind](https://kind.sigs.k8s.io/) on your local machine. See below for an example of using kind. -1. A Kubernetes cluster of a type supported by Pinniped Supervisor (this can be the same cluster as the above, or different). +1. A Kubernetes cluster of a type supported by Pinniped Supervisor (this can be the same cluster as the first, or different). -1. A kubeconfig that has admin-like privileges on each cluster. +1. A kubeconfig that has administrator-like privileges on each cluster. 1. An external OIDC identity provider to use as the source of identity for Pinniped. ## Overview -Installing and trying Pinniped on any cluster will consist of the following general steps. See the next section below +Installing and trying Pinniped on any cluster consists of the following general steps. See the next section below for a more specific example, including the commands to use for that case. -1. Install the Pinniped Supervisor. See [deploy/supervisor/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/supervisor/README.md). +1. [Install the Supervisor]({{< ref "../howto/install-supervisor" >}}). 1. Create a [`FederationDomain`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc#k8s-api-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-federationdomain) via the installed Pinniped Supervisor. @@ -39,31 +38,31 @@ for a more specific example, including the commands to use for that case. 1. Create a [`JWTAuthenticator`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc#k8s-api-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-jwtauthenticator) via the installed Pinniped Concierge. -1. Download the Pinniped CLI from [Pinniped's github Releases page](https://github.com/vmware-tanzu/pinniped/releases/latest). -1. Generate a kubeconfig using the Pinniped CLI. Run `pinniped get kubeconfig --help` for more information. -1. Run `kubectl` commands using the generated kubeconfig. The Pinniped Supervisor and Concierge will automatically be used for authentication during those commands. +1. [Install the Pinniped command-line tool]({{< ref "../howto/install-cli" >}}). +1. Generate a kubeconfig using the Pinniped command-line tool. Run `pinniped get kubeconfig --help` for more information. +1. Run `kubectl` commands using the generated kubeconfig. The Pinniped Supervisor and Concierge are automatically used for authentication during those commands. -## Example of Deploying on Multiple kind Clusters +## Example of deploying on multiple kind clusters [kind](https://kind.sigs.k8s.io) is a tool for creating and managing Kubernetes clusters on your local machine -which uses Docker containers as the cluster's "nodes". This is a convenient way to try out Pinniped on local +which uses Docker containers as the cluster's nodes. This is a convenient way to try out Pinniped on local non-production clusters. -The following steps will deploy the latest release of Pinniped on kind. It will deploy the Pinniped +The following steps deploy the latest release of Pinniped on kind. They deploy the Pinniped Supervisor on one cluster, and the Pinniped Concierge on another cluster. A multi-cluster deployment -strategy is typical for Pinniped. The Pinniped Concierge will use a +strategy is typical for Pinniped. The Pinniped Concierge uses a [`JWTAuthenticator`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc#k8s-api-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-jwtauthenticator) to authenticate federated identities from the Supervisor. 1. Install the tools required for the following steps. - - [Install kind](https://kind.sigs.k8s.io/docs/user/quick-start/), if not already installed. e.g. `brew install kind` on MacOS. + - [Install kind](https://kind.sigs.k8s.io/docs/user/quick-start/), if not already installed. For example, `brew install kind` on macOS. - - kind depends on Docker. If not already installed, [install Docker](https://docs.docker.com/get-docker/), e.g. `brew cask install docker` on MacOS. + - kind depends on Docker. If not already installed, [install Docker](https://docs.docker.com/get-docker/), for example `brew cask install docker` on macOS. - This demo requires `kubectl`, which comes with Docker, or can be [installed separately](https://kubernetes.io/docs/tasks/tools/install-kubectl/). - - This demo requires `openssl`, which is installed on MacOS by default, or can be [installed separately](https://www.openssl.org/). + - This demo requires `openssl`, which is installed on macOS by default, or can be [installed separately](https://www.openssl.org/). 1. Create a new Kubernetes cluster for the Pinniped Supervisor using `kind create cluster --name pinniped-supervisor`. @@ -72,10 +71,10 @@ to authenticate federated identities from the Supervisor. 1. Deploy the Pinniped Supervisor with a valid serving certificate and network path. See [deploy/supervisor/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/supervisor/README.md). - For purposes of this demo, the following issuer will be used. This issuer is specific to DNS and - TLS infrastructure set up for this demo. + For purposes of this demo, the following issuer is used. This issuer is specific to DNS and + TLS infrastructure set up for this demo: - ```bash + ```sh issuer=https://my-supervisor.demo.pinniped.dev ``` @@ -92,7 +91,7 @@ to authenticate federated identities from the Supervisor. [`FederationDomain`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc#k8s-api-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-federationdomain) object to configure the Pinniped Supervisor to issue federated identities. - ```bash + ```sh cat <}}) for instructions on how to deploy using `ytt`. - If you prefer to install a specific version, replace `latest` in the above URL with the version number such as `v0.4.1`. - 1. Generate a random audience value for this cluster. - ```bash + ```sh audience="$(openssl rand -hex 8)" ``` @@ -166,7 +163,7 @@ to authenticate federated identities from the Supervisor. [`JWTAuthenticator`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc#k8s-api-go-pinniped-dev-generated-1-19-apis-concierge-authentication-v1alpha1-jwtauthenticator) object to configure the Pinniped Concierge to authenticate using the Pinniped Supervisor. - ```bash + ```sh cat <}}) for more details. 1. Generate a kubeconfig for the current cluster. - ```bash + + ```sh pinniped get kubeconfig \ --kubeconfig-context kind-pinniped-concierge \ > /tmp/pinniped-kubeconfig ``` - If you are using MacOS, you may get an error dialog that says - `“pinniped” cannot be opened because the developer cannot be verified`. Cancel this dialog, open System Preferences, - click on Security & Privacy, and click the Allow Anyway button next to the Pinniped message. - Run the above command again and another dialog will appear saying - `macOS cannot verify the developer of “pinniped”. Are you sure you want to open it?`. - Click Open to allow the command to proceed. +1. Try using the generated kubeconfig to issue arbitrary `kubectl` commands. The `pinniped` command-line tool + opens a browser page that can be used to login to the external OIDC identity provider configured earlier. -1. Try using the generated kubeconfig to issue arbitrary `kubectl` commands. The `pinniped` CLI will - open a browser page that can be used to login to the external OIDC identity provider configured earlier. - - ```bash + ```sh kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped-concierge ``` @@ -213,30 +208,28 @@ to authenticate federated identities from the Supervisor. to the upstream OIDC identity provider. However, this does prove that you are authenticated and acting as the `pinny` user. -1. As the admin user, create RBAC rules for the test user to give them permissions to perform actions on the cluster. +1. As the administrator user, create RBAC rules for the test user to give them permissions to perform actions on the cluster. For example, grant the test user permission to view all cluster resources. - ```bash + ```sh kubectl --context kind-pinniped-concierge create clusterrolebinding pinny-can-read --clusterrole view --user pinny ``` 1. Use the generated kubeconfig to issue arbitrary `kubectl` commands as the `pinny` user. - ```bash + ```sh kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped-concierge ``` The user has permission to list pods, so the command succeeds this time. - Pinniped has provided authentication into the cluster for your `kubectl` command! 🎉 + Pinniped has provided authentication into the cluster for your `kubectl` command. 🎉 1. Carry on issuing as many `kubectl` commands as you'd like as the `pinny` user. - Each invocation will use Pinniped for authentication. + Each invocation uses Pinniped for authentication. You may find it convenient to set the `KUBECONFIG` environment variable rather than passing `--kubeconfig` to each invocation. - ```bash + ```sh export KUBECONFIG=/tmp/pinniped-kubeconfig kubectl get namespaces kubectl get pods -A ``` - -1. Profit! 💰 diff --git a/site/content/docs/concierge-only-demo.md b/site/content/docs/tutorials/concierge-only-demo.md similarity index 62% rename from site/content/docs/concierge-only-demo.md rename to site/content/docs/tutorials/concierge-only-demo.md index deecfb0f..22943c84 100644 --- a/site/content/docs/concierge-only-demo.md +++ b/site/content/docs/tutorials/concierge-only-demo.md @@ -1,16 +1,15 @@ --- -title: "Pinniped Concierge Only Demo" +title: Learn to use the Pinniped Concierge +description: See how the Pinniped Concierge works to provide a uniform login flow across different Kubernetes clusters. cascade: layout: docs menu: docs: name: Concierge with Webhook - parent: demo + parent: tutorials weight: 100 --- -# Trying Pinniped Concierge - ## Prerequisites 1. A Kubernetes cluster of a type supported by Pinniped as described in [architecture](/docs/architecture). @@ -25,48 +24,53 @@ menu: by following the directions in [deploy/local-user-authenticator/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/local-user-authenticator/README.md). See below for an example of deploying this on kind. -1. A kubeconfig where the current context points to the cluster and has admin-like +1. A kubeconfig where the current context points to the cluster and has administrator-like privileges on that cluster. ## Overview -Installing and trying Pinniped on any cluster will consist of the following general steps. See the next section below +Installing and trying the Pinniped Concierge on any cluster consists of the following general steps. See the next section below for a more specific example of installing onto a local kind cluster, including the exact commands to use for that case. -1. Install the Pinniped Concierge. See [deploy/concierge/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/concierge/README.md). -1. Download the Pinniped CLI from [Pinniped's github Releases page](https://github.com/vmware-tanzu/pinniped/releases/latest). -1. Generate a kubeconfig using the Pinniped CLI. Run `pinniped get kubeconfig --help` for more information. -1. Run `kubectl` commands using the generated kubeconfig. The Pinniped Concierge will automatically be used for authentication during those commands. +1. [Install the Concierge]({{< ref "../howto/install-concierge" >}}). +1. [Install the Pinniped command-line tool]({{< ref "../howto/install-cli" >}}). +1. Configure the Concierge with a + [JWT]({{< ref "../howto/configure-concierge-jwt" >}}) or + [webhook]({{< ref "../howto/configure-concierge-webhook" >}}) authenticator. +1. Generate a kubeconfig using the Pinniped command-line tool (run `pinniped get kubeconfig --help` for more information). +1. Run `kubectl` commands using the generated kubeconfig. -## Example of Deploying on kind + The Pinniped Concierge is automatically be used for authentication during those commands. + +## Example of deploying on kind [kind](https://kind.sigs.k8s.io) is a tool for creating and managing Kubernetes clusters on your local machine -which uses Docker containers as the cluster's "nodes". This is a convenient way to try out Pinniped on a local +which uses Docker containers as the cluster's nodes. This is a convenient way to try out Pinniped on a local non-production cluster. -The following steps will deploy the latest release of Pinniped on kind using the local-user-authenticator component +The following steps deploy the latest release of Pinniped on kind using the local-user-authenticator component as the authenticator. 1. Install the tools required for the following steps. - - [Install kind](https://kind.sigs.k8s.io/docs/user/quick-start/), if not already installed. e.g. `brew install kind` on MacOS. + - [Install kind](https://kind.sigs.k8s.io/docs/user/quick-start/), if not already installed. For example, `brew install kind` on macOS. - - kind depends on Docker. If not already installed, [install Docker](https://docs.docker.com/get-docker/), e.g. `brew cask install docker` on MacOS. + - kind depends on Docker. If not already installed, [install Docker](https://docs.docker.com/get-docker/), for example `brew cask install docker` on macOS. - This demo requires `kubectl`, which comes with Docker, or can be [installed separately](https://kubernetes.io/docs/tasks/tools/install-kubectl/). - - This demo requires a tool capable of generating a `bcrypt` hash in order to interact with + - This demo requires a tool capable of generating a `bcrypt` hash to interact with the webhook. The example below uses `htpasswd`, which is installed on most macOS systems, and can be - installed on some Linux systems via the `apache2-utils` package (e.g., `apt-get install + installed on some Linux systems via the `apache2-utils` package (for example, `apt-get install apache2-utils`). 1. Create a new Kubernetes cluster using `kind create cluster`. Optionally provide a cluster name using the `--name` flag. - kind will automatically update your kubeconfig to point to the new cluster as a user with admin-like permissions. + kind automatically updates your kubeconfig to point to the new cluster as a user with administrator-like permissions. 1. Deploy the local-user-authenticator app. This is a demo authenticator. In production, you would configure an authenticator that works with your real identity provider, and therefore would not need to deploy or configure local-user-authenticator. - ```bash + ```sh kubectl apply -f https://get.pinniped.dev/latest/install-local-user-authenticator.yaml ``` @@ -75,11 +79,11 @@ as the authenticator. see [deploy/local-user-authenticator/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/local-user-authenticator/README.md) for instructions on how to deploy using `ytt`. - If you prefer to install a specific version, replace `latest` in the above URL with the version number such as `v0.4.1`. + If you prefer to install a specific version, replace `latest` in the URL with the version number such as `v0.4.1`. 1. Create a test user named `pinny-the-seal` in the local-user-authenticator namespace. - ```bash + ```sh kubectl create secret generic pinny-the-seal \ --namespace local-user-authenticator \ --from-literal=groups=group1,group2 \ @@ -88,7 +92,7 @@ as the authenticator. 1. Fetch the auto-generated CA bundle for the local-user-authenticator's HTTP TLS endpoint. - ```bash + ```sh kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator \ -o jsonpath={.data.caCertificate} \ | tee /tmp/local-user-authenticator-ca-base64-encoded @@ -96,12 +100,12 @@ as the authenticator. 1. Deploy the Pinniped Concierge. - ```bash + ```sh kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml ``` The `install-pinniped-concierge.yaml` file includes the default deployment options. - If you would prefer to customize the available options, please see [deploy/concierge/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/concierge/README.md) + If you would prefer to customize the available options, please see the [Concierge installation guide]({{< ref "../howto/install-concierge" >}}) for instructions on how to deploy using `ytt`. 1. Create a `WebhookAuthenticator` object to configure the Pinniped Concierge to authenticate using local-user-authenticator. @@ -119,61 +123,63 @@ as the authenticator. EOF ``` -1. Download the latest version of the Pinniped CLI binary for your platform - from Pinniped's [latest release](https://github.com/vmware-tanzu/pinniped/releases/latest). +1. Download the latest version of the Pinniped command-line tool for your platform. + On macOS or Linux, you can do this using Homebrew: -1. Move the Pinniped CLI binary to your preferred filename and directory. Add the executable bit, - e.g. `chmod +x /usr/local/bin/pinniped`. - -1. Generate a kubeconfig for the current cluster. Use `--static-token` to include a token which should - allow you to authenticate as the user that you created above. - - ```bash - pinniped get kubeconfig --static-token "pinny-the-seal:password123" --concierge-authenticator-type webhook --concierge-authenticator-name local-user-authenticator > /tmp/pinniped-kubeconfig + ```sh + brew install vmware-tanzu/pinniped/pinniped-cli ``` - If you are using MacOS, you may get an error dialog that says - `“pinniped” cannot be opened because the developer cannot be verified`. Cancel this dialog, open System Preferences, - click on Security & Privacy, and click the Allow Anyway button next to the Pinniped message. - Run the above command again and another dialog will appear saying - `macOS cannot verify the developer of “pinniped”. Are you sure you want to open it?`. - Click Open to allow the command to proceed. + On other platforms, see the [command-line installation guide]({{< ref "../howto/install-cli" >}}) for more details. + +1. Generate a kubeconfig for the current cluster. Use `--static-token` to include a token which should + allow you to authenticate as the user that you created previously. + + ```sh + pinniped get kubeconfig \ + --static-token "pinny-the-seal:password123" \ + --concierge-authenticator-type webhook \ + --concierge-authenticator-name local-user-authenticator \ + > /tmp/pinniped-kubeconfig + ``` 1. Try using the generated kubeconfig to issue arbitrary `kubectl` commands as the `pinny-the-seal` user. - ```bash - kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped-concierge + ```sh + kubectl --kubeconfig /tmp/pinniped-kubeconfig \ + get pods -n pinniped-concierge ``` Because this user has no RBAC permissions on this cluster, the previous command - results in the error `Error from server (Forbidden): pods is forbidden: User "pinny-the-seal" cannot list resource "pods" in API group "" in the namespace "pinniped"`. + results in the error `Error from server (Forbidden): pods is forbidden: User "pinny-the-seal" cannot list resource "pods" in API group "" in the namespace "pinniped-concierge"`. However, this does prove that you are authenticated and acting as the `pinny-the-seal` user. -1. As the admin user, create RBAC rules for the test user to give them permissions to perform actions on the cluster. +1. As the administrator user, create RBAC rules for the test user to give them permissions to perform actions on the cluster. For example, grant the test user permission to view all cluster resources. - ```bash - kubectl create clusterrolebinding pinny-can-read --clusterrole view --user pinny-the-seal + ```sh + kubectl create clusterrolebinding pinny-can-read \ + --clusterrole view \ + --user pinny-the-seal ``` 1. Use the generated kubeconfig to issue arbitrary `kubectl` commands as the `pinny-the-seal` user. - ```bash - kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped-concierge + ```sh + kubectl --kubeconfig /tmp/pinniped-kubeconfig \ + get pods -n pinniped-concierge ``` The user has permission to list pods, so the command succeeds this time. - Pinniped has provided authentication into the cluster for your `kubectl` command! 🎉 + Pinniped has provided authentication into the cluster for your `kubectl` command. 🎉 1. Carry on issuing as many `kubectl` commands as you'd like as the `pinny-the-seal` user. - Each invocation will use Pinniped for authentication. + Each invocation uses Pinniped for authentication. You may find it convenient to set the `KUBECONFIG` environment variable rather than passing `--kubeconfig` to each invocation. - ```bash + ```sh export KUBECONFIG=/tmp/pinniped-kubeconfig kubectl get namespaces kubectl get pods -A ``` - -1. Profit! 💰 diff --git a/site/netlify.toml b/site/netlify.toml index 75cbcf73..ba32a73e 100644 --- a/site/netlify.toml +++ b/site/netlify.toml @@ -28,3 +28,8 @@ HUGO_VERSION = "0.78.0" [context.next.environment] HUGO_ENABLEGITINFO = "true" + +[[headers]] + for = "/fonts/*" + [headers.values] + Access-Control-Allow-Origin = "*" diff --git a/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content b/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content index 96023260..25ab9b43 100644 --- a/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content +++ b/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.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}.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}.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 .left-links{padding:0px;float:left}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 .right-links{float:right}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}@media only screen and (max-width: 767px){footer .left-links{width:100%;float:none}footer .left-links li{display:block;width:33%;float:left;padding-right:0px}footer .right-links{width:100%;padding-top:20px}footer .right-links .image{display:none}}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}.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}.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:14px;color:#333;line-height:1.6em;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{background-color:#fff;color:#333;border:2px solid #EFEFEF;padding:2px 8px}.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 pre code{display:block;border:15px solid #EFEFEF;padding:15px;margin-bottom:30px}.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}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.docs .side-nav{width:25%;float:left}.docs .side-nav ul{padding-left:0px;margin-bottom:35px}.docs .side-nav ul ul{padding-left:15px;margin-top:10px;margin-bottom:15px}.docs .side-nav ul li{display:list-item;margin-bottom:15px}.docs .side-nav ul li a{color:#333;font-size:14px}.docs .side-nav ul li a.active{color:#0095D3}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .docs-content{width:75%;float:right}.docs .docs-content a{font-size:14px}.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{background-color:#fff;color:#333;border:2px solid #EFEFEF;padding:2px 8px}.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 pre code{display:block;border:15px solid #EFEFEF;padding:15px;margin-bottom:30px}.docs .docs-content img{max-width:100%}.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 .left-links{padding:0px;float:left}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 .right-links{float:right}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}@media only screen and (max-width: 767px){footer .left-links{width:100%;float:none}footer .left-links li{display:block;width:33%;float:left;padding-right:0px}footer .right-links{width:100%;padding-top:20px}footer .right-links .image{display:none}}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}.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{background-color:#fff;color:#333;border:2px solid #EFEFEF;padding:2px 8px}.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 pre code{display:block;border:15px solid #EFEFEF;padding:15px;margin-bottom:30px}.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}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.docs .side-nav{width:25%;float:left}.docs .side-nav ul{padding-left:0px;margin-bottom:35px}.docs .side-nav ul ul{padding-left:15px;margin-top:10px;margin-bottom:15px}.docs .side-nav ul li{display:list-item;margin-bottom:15px}.docs .side-nav ul li a{color:#333;font-size:14px}.docs .side-nav ul li a.active{color:#0095D3}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .docs-content{width:75%;float:right}.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{background-color:#fff;color:#333;border:2px solid #EFEFEF;padding:2px 8px}.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 pre code{display:block;border:15px solid #EFEFEF;padding:15px;margin-bottom:30px;font-size:14px}.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 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/_base.scss b/site/themes/pinniped/assets/scss/_base.scss index f10c3d71..2b329929 100644 --- a/site/themes/pinniped/assets/scss/_base.scss +++ b/site/themes/pinniped/assets/scss/_base.scss @@ -97,6 +97,13 @@ button { &.tertiary { border: 1px solid $blue; } + img.button-icon { + margin-left: 5px; + margin-right: 5px; + margin-bottom: -8px; + width: 24px; + height: 24px; + } } .buttons { margin-top: 40px; diff --git a/site/themes/pinniped/assets/scss/_components.scss b/site/themes/pinniped/assets/scss/_components.scss index b62de86d..60b65d28 100644 --- a/site/themes/pinniped/assets/scss/_components.scss +++ b/site/themes/pinniped/assets/scss/_components.scss @@ -273,9 +273,8 @@ list-style-type: unset; display: list-item; margin-bottom: 10px; - font-size: 14px; + font-size: 16px; color: $darkgrey; - line-height: 1.6em; list-style-image: url(/img/arrow.svg); } } @@ -437,7 +436,7 @@ width: 75%; float: right; a { - font-size: 14px; + font-size: 16px; } ul { list-style-type: disc; @@ -491,10 +490,36 @@ border: 15px solid #EFEFEF; padding: 15px; margin-bottom: 30px; + font-size: 14px; } } img { max-width: 100%; } + strong { + font-family: $metropolis-medium; + } + } + .danger { + .danger-icon { + float: left; + padding: 40px; + width: 24px; + height: 24px; + } + padding: 10px; + font-family: $metropolis-light-italic; + } + + .button { + a { + font-size: 14px; + } + } + + table { + td { + padding: 10px 30px; + } } } \ No newline at end of file diff --git a/site/themes/pinniped/layouts/_default/docs.html b/site/themes/pinniped/layouts/_default/docs.html index 818bc2e5..c223ff91 100644 --- a/site/themes/pinniped/layouts/_default/docs.html +++ b/site/themes/pinniped/layouts/_default/docs.html @@ -7,8 +7,10 @@
{{ partial "docs-sidebar.html" . }} -
+ {{- with .Title -}} +

{{.}}

+ {{- end -}} {{ .Content }}
diff --git a/site/themes/pinniped/layouts/shortcodes/buttonicon.html b/site/themes/pinniped/layouts/shortcodes/buttonicon.html new file mode 100644 index 00000000..14458a77 --- /dev/null +++ b/site/themes/pinniped/layouts/shortcodes/buttonicon.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/themes/pinniped/layouts/shortcodes/buttonlink.html b/site/themes/pinniped/layouts/shortcodes/buttonlink.html new file mode 100644 index 00000000..7895fc5e --- /dev/null +++ b/site/themes/pinniped/layouts/shortcodes/buttonlink.html @@ -0,0 +1,2 @@ +{{- $href := .Get "href" -}} +
{{.Inner}}
\ No newline at end of file diff --git a/site/themes/pinniped/layouts/shortcodes/dangernote.html b/site/themes/pinniped/layouts/shortcodes/dangernote.html new file mode 100644 index 00000000..a97ae3df --- /dev/null +++ b/site/themes/pinniped/layouts/shortcodes/dangernote.html @@ -0,0 +1,4 @@ +
+ +

{{ .Inner | markdownify }}

+
diff --git a/site/themes/pinniped/layouts/shortcodes/docsmenu.html b/site/themes/pinniped/layouts/shortcodes/docsmenu.html new file mode 100644 index 00000000..13878b6b --- /dev/null +++ b/site/themes/pinniped/layouts/shortcodes/docsmenu.html @@ -0,0 +1,15 @@ +{{- $subsection := .Get 0 -}} + diff --git a/site/themes/pinniped/static/img/alert-circle-outline.svg b/site/themes/pinniped/static/img/alert-circle-outline.svg new file mode 100644 index 00000000..de22ac8f --- /dev/null +++ b/site/themes/pinniped/static/img/alert-circle-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/themes/pinniped/static/img/download.png b/site/themes/pinniped/static/img/download.png new file mode 100644 index 00000000..c7557804 Binary files /dev/null and b/site/themes/pinniped/static/img/download.png differ