Let’s say you’re going on a trip for few weeks and you prefer not to take your laptop with you. But you manage some small kubernetes cluster which is behind Cloudflare Zero Trust which can be only accessed:

And you want to have some sort of emergency access while you’re away from your iPad.

You can do it using Github Codespaces, but whole process is not well documented, let’s start with some references which were helpful when I glued all this hack together:

First what you wanna do is to configure zero trust

You gonna need a service token:

resource "cloudflare_zero_trust_access_service_token" "warp_codespaces_cli" {
  account_id = var.cloudflare_account_id
  name       = "warp-cli-service-token"
  duration   = "forever"
}

A new policy:

resource "cloudflare_zero_trust_access_policy" "warp_cli" {
  account_id       = var.cloudflare_account_id
  name             = "warp-enrollment-cli"
  session_duration = "24h"
  decision         = "non_identity"

  include {
    service_token = [cloudflare_zero_trust_access_service_token.warp_codespaces_cli.id]
  }
}

Moreover - you will need a to link this policy to your existing warp settings:

resource "cloudflare_zero_trust_access_application" "warp" {
  account_id           = var.cloudflare_account_id
  name                 = "Warp Login App"
  domain               = "<your domain>.cloudflareaccess.com/warp"
  app_launcher_visible = false
  type                 = "warp"

  policies = [
    < any existing policies >
    cloudflare_zero_trust_access_policy.warp_cli.id
  ]
}

So your Device enrollment permissions should look like this:

I don’t recall how I created profile settings for the CLI, but I think that was one thing I did manually:

Then you need some stuff in .devcontainer/ directory

devcontainer.json example:

{
    "name": "Development Container",
    "build": {
        "dockerfile": "Dockerfile",
        "context": ".."
    },
    "customizations": {
        "vscode": {
            "extensions": [
                "redhat.ansible",
                "ms-python.python",
                "ms-azuretools.vscode-docker"
            ],
            "settings": {
                "terminal.integrated.profiles.linux": {
                    "bash": {
                        "path": "/bin/bash",
                        "icon": "terminal-bash"
                    }
                },
                "terminal.integrated.defaultProfile.linux": "bash"
            }
        }
    },
    "workspaceFolder": "/workspaces",
    "secrets": {
        "WARP_CLIENT_ID": {
            "description": "WARP client id, provided by organization secret"
        },
        "WARP_CLIENT_SECRET": {
            "description": "WARP client secret, provided by organization secret"
        },
        "OIDC_CLIENT_SECRET": {
            "description": "OIDC client secret, provided by organization secret"
        }
    },
    "capAdd": ["NET_ADMIN"],
    "remoteUser": "vscode"
}

You will want to set secrets in the codespaces settings. Warp id/secret is obtained from warp_codespaces_cli (if created via terraform). You can remove OIDC if you’re not using kubelogin.

Then goes Dockerfile - I used python as I also had ansible in that repo:

FROM mcr.microsoft.com/devcontainers/python:3.13

RUN apt-get update && apt-get install -y --no-install-recommends \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release \
    git \
    iputils-ping \
    iptables \
    && rm -rf /var/lib/apt/lists/*

# Install Cloudflare WARP client
RUN curl https://pkg.cloudflareclient.com/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg \
    && echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" \
        > /etc/apt/sources.list.d/cloudflare-client.list \
    && apt-get update && apt-get install -y cloudflare-warp \
    && rm -rf /var/lib/apt/lists/*

# Install kubectl and k9s
ARG KUBECTL_VERSION=v1.33.1
ARG K9S_VERSION=v0.50.9

RUN set -eux; \
    ARCH="$(uname -m)"; \
    if [ "$ARCH" = "x86_64" ]; then \
    KARCH="amd64"; \
    elif [ "$ARCH" = "aarch64" ] || echo "$ARCH" | grep -q '^arm'; then \
    KARCH="arm64"; \
    else \
    echo "Unsupported architecture: $ARCH"; exit 1; \
    fi; \
    curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${KARCH}/kubectl" \
    && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl \
    && rm kubectl \
    && curl -L "https://github.com/derailed/k9s/releases/download/${K9S_VERSION}/k9s_Linux_${KARCH}.tar.gz" -o k9s.tar.gz \
    && tar -zxvf k9s.tar.gz -C /usr/local/bin k9s \
    && rm k9s.tar.gz

USER vscode

# Install krew and oidc-login plugin under vscode user
ARG KREW_VERSION=v0.4.5

RUN ARCH="$(uname -m)"; \
    if [ "$ARCH" = "x86_64" ]; then \
    KARCH="amd64"; \
    elif [ "$ARCH" = "aarch64" ] || echo "$ARCH" | grep -q '^arm'; then \
    KARCH="arm64"; \
    else \
    echo "Unsupported architecture: $ARCH"; exit 1; \
    fi; \
    curl -L "https://github.com/kubernetes-sigs/krew/releases/download/${KREW_VERSION}/krew-linux_${KARCH}.tar.gz" -o /tmp/krew.tar.gz \
    && tar -zxvf /tmp/krew.tar.gz -C /tmp \
    && /tmp/krew-linux_${KARCH} install krew \
    && rm /tmp/krew.tar.gz \
    && echo 'export PATH="${HOME}/.krew/bin:$PATH"' >> ~/.bashrc \
    && export PATH="${HOME}/.krew/bin:$PATH" \
    && kubectl krew install oidc-login \
    && rm -rf ~/.krew/tmp

That should give you a container with python, kubectl, k9s, Cloudflare WARP and kubelogin ready to go in Codespaces - fire it up and see!

Now what’s left is to glue it all together, I decided to go with a simple bash script which fires all this machinery up:

#!/bin/bash
if [ ! -e /dev/net/tun ]; then
    echo "- TUN device not found, creating..."
    sudo mkdir -p /dev/net
    sudo mknod /dev/net/tun c 10 200
    sudo chmod 600 /dev/net/tun
fi

echo "- Starting dbus service..."
sudo service dbus start

# give dbus some time to start
sleep 3

# Check if envs are set
if [ -z "$WARP_CLIENT_ID" ] || [ -z "$WARP_CLIENT_SECRET" ]; then
    echo "WARP_CLIENT_ID and WARP_CLIENT_SECRET must be set as environment variables."
    exit 1
fi

file_content=$(cat <<EOF
<dict>
  <key>organization</key>
  <string>!!! --- PUT YOUR CLOUDFLARE ORG NAME HERE -- !!!</string>
  <key>auth_client_id</key>
  <string>${WARP_CLIENT_ID}</string>
  <key>auth_client_secret</key>
  <string>${WARP_CLIENT_SECRET}</string>
</dict>
EOF
)
sudo mkdir -p /var/lib/cloudflare-warp
echo "$file_content" | sudo tee /var/lib/cloudflare-warp/mdm.xml > /dev/null

echo "- Starting Cloudflare WARP service (logs at /tmp/cloudflare-warp)..."
sudo warp-svc --accept-tos >/tmp/cloudflare-warp 2>&1 &

# give warp some time to start
echo "- Waiting for Cloudflare WARP to start..."
sleep 5

echo "- Connecting to Cloudflare WARP..."
warp-cli --accept-tos connect

if [ -z "$OIDC_CLIENT_SECRET" ]; then
    echo "OIDC_CLIENT_SECRET must be set as an environment variable."
    exit 1
fi

# I keep kube-config in my repo, reference below
echo "- Configuring kube config..."
mkdir -p ~/.kube
envsubst < .devcontainer/kube-config.yaml > ~/.kube/config

echo "All set! You can now access your Kubernetes cluster using k9s."

Run that while in codespace’s terminal and that will configure Cloudflare WARP and now you should be able to access your internal network from codespaces container which is pretty neat-o.

For reference my kube-config.yaml (truncated)

# ...
clusters:
- cluster:
    certificate-authority-data: Redacted
    server: https://<internal network ip>:6443
  name: my-cluster
# ...
users:
- name: my-oidc
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - oidc-login
      - get-token
      - --oidc-issuer-url=https://accounts.google.com
      - --oidc-extra-scope=email
      - --oidc-client-id=<<MY CLIENT ID>>
      - --oidc-client-secret=${OIDC_CLIENT_SECRET}
      - --grant-type=device-code
      command: kubectl
      env: null
      interactiveMode: IfAvailable
      provideClusterInfo: false

Remember if you want to use device-code you need to configure oauth for limited input - as there is no way to open a browser while in codespaces container, but PIN login still should work nicely.