8 min read

Deploying an OAuth2 Proxy to Enable Zero Trust Authentication with Humanitec and Score

Deploying an OAuth2 Proxy to Enable Zero Trust Authentication with Humanitec and Score
Deploying an OAuth2 Proxy to Enable Zero Trust Authentication with Humanitec and Score

In today’s evolving cybersecurity landscape, a zero trust architecture has become a cornerstone for protecting modern applications. By assuming that every network request could potentially originate from a hostile source, zero trust enforces strict identity verification at every layer. This article will walk you through deploying an OAuth2 proxy using oauth2-proxy to enable robust authentication for your application. We’ll integrate it seamlessly into a workload deployed via Humanitec using the Score workload specification.

What is a Zero Trust Architecture and what do you need to enable it?

Zero trust architecture is a security model that assumes no user or system is inherently trustworthy, regardless of their location within or outside the network. The fundamental principles include:

  • Identity and Access Management (IAM): Every user and system interaction must be authenticated and authorized.
  • Least Privilege Access: Users and systems are granted the minimum level of access necessary.
  • Continuous Monitoring: All interactions are constantly monitored to detect and respond to threats.

To enable these traits in an existing system, without huge modifications to the original system, you need:

  1. An identity provider (IdP) for managing users and roles.
  2. An authentication proxy to enforce identity verification.
  3. A means of integrating authentication seamlessly with your application’s deployment.

This is where the oauth2-proxy comes in.

Introducing OAuth2-Proxy: The ideal authentication layer

oauth2-proxy is a lightweight reverse proxy that acts as a gateway between your application and identity providers. It supports several IdPs, such as Microsoft Entra ID, Google, GitHub, and Okta, making it a versatile solution for enforcing authentication.

Key Capabilities:

  • Simple Integration: Easily integrates with Kubernetes, containerized applications, and cloud platforms.
  • Support for Multiple Identity Providers: Handles various OAuth2/OIDC providers with ease.
  • Session Management: Leverages backend session storage (e.g., Redis) for secure and scalable session handling.
  • Customizable Authorization: Enables fine-grained control over user access through route-level rules.

Given its flexibility and focus on authentication, oauth2-proxy is an excellent choice to externalize the concern of authentication from your existing applications.

Creating a Score-Based Demo Application with Humanitec

Let’s set up a basic application to demonstrate the integration. We’ll use humctl to initialize the deployment and configure the application workload using Score.

Step 1: Initialize the Application

Start by creating a new Humanitec application using the demo app repository:

humctl create app secure-app

Clone the demo-app repository - it already is Score enabled:

git clone https://github.com/astromechza/demo-app.git
cd demo-app

The score.yaml file in this repository defines the application’s workload specification. We’ll modify it later to include the oauth2-proxy setup. If you want to enable your own project to use Score, you can simply execute humctl score init to get it started.

Step 2: Deploy the Base Application

Deploy the application with Humanitec to ensure everything works as expected:

humctl score deploy -f score.yaml -i ghcr.io/astromechza/demo-app:latest --app secure-app --env development --org ###YourOrgHere###

If you do not have access to a Humanitec organization/tenant, you can enlist for a free trial here.

Take note of the -i flag to inject the image into the Score file. You could also replace the . in the Score file with the image-tag and ommit it during the deployment command. The idea here is, that a CI pipeline or local automation can inject the image tag that it just did build and push before more easily.

Adding OAuth2 Proxy to the Workload

Now, we’ll modify the score.yaml to include the oauth2-proxy - developers will be able to trigger this on their own services or applications by setting just an annotation. The proxy will be added as a sidecar to the application container. But for that to happen we need another type of artefact for Humanitec.

Score files are usually used by developers to describe what their application asks of the platform to be present in a runtime environment before deployment. Platform engineers describe how to create the answer to the ask with resource definitions. We need two of them to make this work. First the workload resource that will modify the actual workload and add the sidecar container. The second one being a config resource that allows us to get the configuration for the OAuth2-Proxy from an attached secret store.

Step 1: Ask for authentication to be added to your service

Add an annotation to the service in your score.yaml file to indicate that authentication is required. This annotation will trigger the injection of the oauth2-proxy sidecar:

metadata:
  annotations:
    add-oauth2-proxy: required

Step 2: Configuring Humanitec - understanding the basics

For this, we need a resource definition. Resource definitions configure the orchestrator to react in a certain way under defined conditions. Lets first create a very simple workload resource definition that will add another annotation to demonstrate what it can do.

You can think of it, like it is a way to introduce meta-programming into your deployment model. You can modify the actual workload before it is rendered into the it's final manifest form for deployment, injecting a cross-cutting-concern like authentication.

Essentially a workload resource definition would be a pointcut (where to apply the modification) and the advice (what to modify) rolled into one concise format.

We'll be using Terraform to make consistent deployment of more than one resource definition more easy. As plumbing, we're creating two files to configure Terraform:

providers.tf:

terraform {
  required_providers {
    humanitec = {
      source  = "humanitec/humanitec"
      version = "~> 1.6"
    }
  }
  required_version = ">= 1.5.7"

}

variable.tf:

variable "prefix" {
  description = "Prefix of the created resources"
  type        = string
  default     = "clearcode-au2p-test"
}

Let's take a look at this simple example, before we work on the real thing - save it as clearcode-add-annotation.tf - we'll be using Terraform here to make it more easy to apply all needed resource definitions at once later (if you want a more complex or complete example, you can check here):

resource "humanitec_resource_definition" "clearcode-add-annotation" {
  driver_type = "humanitec/template"
  id          = "clearcode-add-annotation"
  name        = "clearcode-add-annotation"
  type        = "workload"
  driver_inputs = {
    values_string = jsonencode({
      "templates" = {
        "outputs" = <<END_OF_TEXT
update:
  - op: add
    path: /spec/deployment/annotations
    value:
      env_id: $${context.env.id}
END_OF_TEXT
      }
    })
  }
}

resource "humanitec_resource_definition_criteria" "clearcode-add-annotation_0" {
  resource_definition_id = resource.humanitec_resource_definition.clearcode-add-annotation.id

}

It uses a notation called JSON Patch to describe what needs to changed in the workload before it's rendered into it's final manifest form. We simply add an annotation that we fill with information from the context that the Humanitec orchestrator provides.

Feel free to add this to the orchestrator's configuration and redeploy to see the outcome on the final pod - you can check with kubectl, but I don't know the name of your namespace or pod, so please do this on your own.

# Upload the res-def to the orchestrator
terraform init
terraform apply -auto-approve

# Redeploy application
humctl score deploy -f score.yaml -i ghcr.io/astromechza/demo-app:latest --app secure-app --env development --org ###YourOrgHere###

Step 3: Injecting the oauth2-proxy sidecar

Now we'll do the same, but we'll add the whole oauth2-proxy sidecar - save this as main.tf:

resource "humanitec_resource_definition" "oauth2-proxy-sidecar" {
  driver_type = "humanitec/template"
  id          = "${var.prefix}-sidecar"
  name        = "${var.prefix}-sidecar"
  type        = "workload"
  driver_inputs = {
    values_string = jsonencode({
      "templates" = {
        "outputs" = <<END_OF_TEXT
          update: []
          test: "{{- dig "spec" "annotations" "add-oauth2-proxy" "false" .resource }}"
END_OF_TEXT
        "manifests" = {
          "sidecar.yaml" = {
            "location" = "containers"
            "data"     = <<END_OF_TEXT
{{- if eq "true" (dig "spec" "annotations" "add-oauth2-proxy" "false" .resource) }}
{{- /*
  The OAuth2-Proxy container as a sidecar in the workload
*/ -}}
image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1-alpine
imagePullPolicy: IfNotPresent
name: oauth2-proxy
resources:
  limits:
    cpu: 100m
    memory: 100Mi
  requests:
    cpu: 100m
    memory: 100Mi
ports:
- containerPort: 3000
args:
- --provider=azure
{{- else}}
name: pause-sso
image: k8s.gcr.io/pause:3.9
imagePullPolicy: IfNotPresent
{{- end }}
END_OF_TEXT
          }
        }
      }
    })
  }
}

Notice how this doesn't work 🤦 we're missing quite some configuration here! We have no backing session store and no provider configuration apart from --provider=azure which is just a suggestion that you can change to your own.

Step 4: Getting Redis as session cache and secrets

Let's work on the missing backing Redis first, as I can cover that for you. Let's add a redis.yaml before the sidecar.yaml:

"redis.yaml" = {
            "location" = "containers"
            "data"     = <<END_OF_TEXT
{{- if eq "true" (dig "spec" "annotations" "add-oauth2-proxy" "false" .resource) }}
image: redis:7.4-alpine
imagePullPolicy: IfNotPresent
name: redis
ports:
- containerPort: 6379
resources:
  limits:
    cpu: 100m
    memory: 100Mi
  requests:
    cpu: 100m
    memory: 100Mi
{{- else}}
name: pause-redis
image: k8s.gcr.io/pause:3.9
imagePullPolicy: IfNotPresent
{{- end }}
END_OF_TEXT
          }

One step closer to a working oauth2-proxy wrapper! Let's get the missing configuration for the Azure example from the secret store and insert it right after the provider:

env:
- name: OAUTH2_PROXY_CLIENT_ID
  valueFrom:
    secretKeyRef:
      name: $${resources['config.default#${humanitec_resource_definition.oauth2-proxy-credentials.id}'].outputs.secret_name}
      key: client-id
- name: OAUTH2_PROXY_CLIENT_SECRET
  valueFrom:
    secretKeyRef:
      name: $${resources['config.default#${humanitec_resource_definition.oauth2-proxy-credentials.id}'].outputs.secret_name}
      key: client-secret
- name: OAUTH2_PROXY_COOKIE_SECRET
  valueFrom:
    secretKeyRef:
      name: $${resources['config.default#${humanitec_resource_definition.oauth2-proxy-credentials.id}'].outputs.secret_name}
      key: cookie-secret

Step 5: Provider configuration for oauth2-proxy

Now you only need the actual provider configuration - this is specific to your provider (maybe you even need to adapt the secrets) and you can simply refer to the awesome oauth2-proxy docs here, to get the provider config that needs to be inserted as well.

Step 6: Config resources - DRY for secrets

Last missing part to complete the setup is the config resource that securely fetches the credentials in the background. The workload resource is just referencing it. This is a complete resource which can be added as well to main.tf:

resource "humanitec_resource_definition" "oauth2-proxy-credentials" {
  driver_type = "humanitec/template"
  id          = "${var.prefix}-credentials"
  name        = "${var.prefix}-credentials"
  type        = "config"
  driver_inputs = {
    secret_refs = jsonencode({
      "client_id" = {
        ref = "oauth2_proxy_cred_client_id"
        store = "my-secret-store"
      }
      "client_secret" = {
        ref = "oauth2_proxy_cred_client_secret"
        store = "my-secret-store"
      }
      "cookie_secret" = {
        ref = "oauth2_proxy_cred_cookie_secret"
        store = "my-secret-store"
      }
    })
    values_string = jsonencode({
      "secret_name" = "oauth2-proxy-creds"
      "templates" = {
        "manifests" = {
          "oauth2-proxy-creds.yaml" = {
            "location" = "namespace"
            "data"     = <<END_OF_TEXT
apiVersion: v1
kind: Secret
metadata:
  name: {{ .driver.values.secret_name }}
data:
  client-id: {{ .driver.secrets.client_id | b64enc | toRawJson}}
  client-secret: {{ .driver.secrets.client_secret | b64enc | toRawJson }}
  cookie-secret: {{ .driver.secrets.cookie_secret | b64enc | toRawJson }}
type: Opaque
END_OF_TEXT
          }
        }
        "outputs" = "secret_name: {{ .driver.values.secret_name }}"
      }
    })
  }
}

Step 7: Matching the resource definitions

To complete your main.tf you need to add some matching criteria as well. This will tell the orchestrator under which conditions exactly these resource definitions should be used instead of any other. Think e.g. "differently sized things in dev and prod" here - so developers don't need to maintain a Score file per environment. Authentication is universal however and even more so in a zero trust architecture, so we'll match very broadly - feel free to trim this down for your own test setup.

resource "humanitec_resource_definition_criteria" "oauth2-proxy-sidecar_criteria_0" {
  resource_definition_id = humanitec_resource_definition.oauth2-proxy-sidecar.id
  class                  = "default"
}

resource "humanitec_resource_definition_criteria" "oauth2-proxy-creds_criteria_0" {
  resource_definition_id = humanitec_resource_definition.oauth2-proxy-credentials.id
  class                  = "default"
}

Step xyz 😈: TLDR - I want the sources!!!

I have prepared a Github repo for you that contains all sources - so you can directly fork this and edit instead of C&P drama.

GitHub - jayonthenet/oauth2-proxy-zta-blog: Backing files for clearco.de blogpost on Zero Trust Architectures, oauth2-proxy, Score and Humanitec
Backing files for clearco.de blogpost on Zero Trust Architectures, oauth2-proxy, Score and Humanitec - jayonthenet/oauth2-proxy-zta-blog

Summary

Integrating oauth2-proxy into your application deployed with Humanitec and Score enables you to enforce zero trust authentication effectively. By leveraging annotations and dynamic resource injection, this approach ensures seamless integration while maintaining flexibility and security.

This method empowers you to:

  • Protect applications with minimal effort.
  • Leverage existing identity providers.
  • Maintain a scalable and secure authentication layer.

With this setup, you’re well on your way to achieving a robust zero trust architecture. Happy deploying!

Next Steps

There will be another post coming, that shows how to externalize the Redis instance, so you don't need to have a ton of them that barely do anything.