KSECRETS picoCTF 2026 Solution

Published: March 20, 2026

Description

We have a kubernetes cluster setup and the flag is in the secrets. You think you can get it?

Launch the challenge instance and download the kubeconfig from the challenge page.

Point kubectl at the downloaded file with: export KUBECONFIG=./kubeconfig

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Set up kubectl with the provided kubeconfig
    Observation
    I noticed the challenge provides a kubeconfig file for a remote Kubernetes cluster, which means all interaction depends on pointing kubectl at that file and having a working API server connection before any enumeration can begin.
    Download the kubeconfig file from the challenge page. Set KUBECONFIG or use --kubeconfig to point kubectl at it. If it fails with a certificate error, add --insecure-skip-tls-verify to your kubectl commands.
    bash
    export KUBECONFIG=./kubeconfig.yaml
    bash
    kubectl get namespaces
    bash
    # If certificate error:
    bash
    kubectl get namespaces --insecure-skip-tls-verify
    bash
    kubectl get secrets --insecure-skip-tls-verify

    Expected output

    picoCTF{k3y_s3cr3ts_4r3n7_s4f3_...}
    What didn't work first

    Tried: Running kubectl get namespaces without setting KUBECONFIG and getting a connection refused error.

    Without KUBECONFIG pointing at the downloaded file, kubectl defaults to ~/.kube/config which either does not exist or points at a different cluster. The command exits with 'The connection to the server localhost:8080 was refused.' Setting 'export KUBECONFIG=./kubeconfig.yaml' before any kubectl call directs it to the challenge cluster.

    Tried: Trusting the TLS certificate error and abandoning kubectl instead of passing --insecure-skip-tls-verify.

    The challenge cluster uses a self-signed certificate that kubectl cannot verify by default, producing 'x509: certificate signed by unknown authority'. Appending --insecure-skip-tls-verify to every kubectl command (or setting 'insecure-skip-tls-verify: true' in the kubeconfig) bypasses the TLS check and lets the connection succeed.

    Learn more

    Kubernetes (k8s) is an open-source container orchestration platform. It organises containerised workloads into pods (groups of containers), and groups pods into namespaces (logical isolation boundaries). kubectl is the command-line client that communicates with the Kubernetes API server.

    A kubeconfig file (typically at ~/.kube/config) stores cluster connection details, credentials, and the current context (which cluster and namespace you are targeting). The kubectl config current-context command shows which context is active. kubectl auth can-i --list shows all actions the current user is permitted to perform - this is the first step in Kubernetes privilege assessment during a penetration test.

    In cloud environments, Kubernetes clusters are commonly managed by cloud providers (Amazon EKS, Google GKE, Azure AKS). Each pod can be assigned a service account with associated permissions. When a pod is compromised, attackers often find the service account token at /var/run/secrets/kubernetes.io/serviceaccount/token and use it to authenticate to the API server.

  2. Step 2
    List Kubernetes secrets
    Observation
    I noticed the challenge description states the flag is stored in Kubernetes secrets, which suggested enumerating all secrets across all namespaces to locate the one holding the flag value.
    Run kubectl get secrets first to see the names. If the flag-bearing secret isn't obvious from the name, dump every secret as YAML and grep for the picoCTF prefix in the base64-decoded data.
    bash
    kubectl get secrets
    bash
    kubectl get secrets --all-namespaces
    bash
    # If the relevant secret isn't obviously named, search every value:
    bash
    kubectl get secrets -o yaml | grep -i flag
    bash
    # Or decode every value to find the one that starts with picoCTF{:
    bash
    kubectl get secrets -o jsonpath='{range .items[*]}{.metadata.name}{": "}{range .data.*}{@}{"\n"}{end}{end}' | while read line; do echo "$line"; done
    What didn't work first

    Tried: Running 'kubectl get secrets' in the default namespace and seeing no secrets listed, then assuming the cluster has no flag.

    The flag secret lives in the 'picoCTF' namespace, not 'default'. Without '--all-namespaces' or '-n picoCTF', kubectl only queries the namespace specified in the kubeconfig context, which is typically 'default'. The fix is 'kubectl get secrets --all-namespaces --insecure-skip-tls-verify' to enumerate every namespace.

    Tried: Grepping the raw 'kubectl get secrets -o yaml' output for 'picoCTF{' and finding nothing.

    Secret values are stored base64-encoded in the YAML output, so the literal string 'picoCTF{' never appears in the raw dump. Grepping for 'flag' in the key names is more reliable at this stage; the actual value must be piped through 'base64 -d' before the CTF prefix becomes visible.

    Learn more

    Kubernetes Secrets are API objects designed to store sensitive data like passwords, tokens, and certificates. They are stored in etcd(the cluster's distributed key-value store) and can be mounted into pods as files or exposed as environment variables. The built-in Secrets object provides namespacing and RBAC-controlled access, but it does not provide encryption by default - values are stored base64-encoded, not encrypted.

    The key Kubernetes RBAC permission for this challenge is get and list on the secrets resource. A cluster that grants broad secret-read permissions to any service account is a common misconfiguration. Real-world Kubernetes security incidents like the Tesla cryptojacking breach (2018) and several CI/CD supply chain attacks exploited overly permissive service account tokens to exfiltrate secrets.

    Kubernetes also supports integration with external secret management systems like HashiCorp Vault (via the Vault Agent Injector or CSI Driver), AWS Secrets Manager (via the External Secrets Operator), and Sealed Secrets (which encrypts secrets as SealedSecret CRDs that are safe to commit to git). These provide encryption at rest and fine-grained access control beyond what native Kubernetes Secrets offer. See the Linux CLI guide for more on enumerating cluster state from the command line.

  3. Step 3
    Retrieve the flag secret and decode it
    Observation
    I noticed the listing revealed a secret named 'ctf-secrets' in the 'picoCTF' namespace, and knowing Kubernetes stores secret values as base64-encoded strings suggested extracting the specific 'flag' key with jsonpath and piping it through base64 -d to obtain the plaintext flag.
    Get the secret in the picoCTF namespace. The value is base64-encoded - pipe it through base64 -d to decode.
    bash
    kubectl get secrets --insecure-skip-tls-verify -n picoCTF
    bash
    kubectl get secret ctf-secrets --insecure-skip-tls-verify -n picoCTF -o yaml
    bash
    kubectl get secret ctf-secrets --insecure-skip-tls-verify -n picoCTF -o jsonpath='{.data.flag}' | base64 -d
    What didn't work first

    Tried: Piping the entire 'kubectl get secret -o yaml' output through 'base64 -d' instead of extracting just the flag field first.

    The YAML envelope (metadata, kind, apiVersion) is plain text, not base64. Passing the whole document to 'base64 -d' produces garbled output or a 'invalid input' error because the decoder encounters non-base64 characters. The correct approach is to extract the single key value with '-o jsonpath={.data.flag}' before decoding.

    Tried: Using 'kubectl get secret flag-secret -n picoCTF' but getting a 'not found' error because the secret is named 'ctf-secrets', not 'flag-secret'.

    The secret name must match exactly what the cluster has. Running 'kubectl get secrets --all-namespaces --insecure-skip-tls-verify' first reveals the real name 'ctf-secrets' in the 'picoCTF' namespace, which avoids guessing and a misleading 'not found' response.

    Learn more

    Kubernetes Secret values are stored base64-encoded in etcd and in the YAML output of kubectl get secret. This encoding is not encryption - it is purely to allow arbitrary binary data to be stored in the YAML format. Anyone with read access to the secret can trivially decode the value with base64 -d. This is why access control on the secrets resource is critical.

    The -o jsonpath flag applies a JSONPath expression to the API response and extracts just the requested field. {.data.flag} navigates to the data map and then the flag key. Piping through base64 -d decodes the value in one command. The equivalent using jq is kubectl get secret flag-secret -o json | jq -r '.data.flag' | base64 -d.

    Encryption at rest for Kubernetes Secrets requires configuring the API server with an EncryptionConfiguration that specifies an encryption provider (AES-GCM, AES-CBC, or a KMS provider backed by an external key management system). Without this configuration, secrets in etcd are only base64-encoded and readable by anyone with direct etcd access. Most managed Kubernetes services (EKS, GKE) enable encryption at rest by default; self-managed clusters must configure it explicitly.

Flag

Reveal flag

picoCTF{k3y_s3cr3ts_4r3n7_s4f3_...}

Use kubectl with --insecure-skip-tls-verify and the provided kubeconfig. Get secrets in the picoCTF namespace, then decode the base64-encoded flag value.

Key takeaway

Kubernetes Secrets are base64-encoded at rest by default, not encrypted, meaning any service account with 'get' or 'list' permission on the secrets resource can read them in plaintext after a trivial decode step. Overly permissive RBAC roles that grant broad secret-read access are among the most common Kubernetes misconfigurations and have been exploited in real-world cloud breaches. The fix is least-privilege RBAC combined with encryption-at-rest configuration on the API server, and ideally an external secret manager (HashiCorp Vault, AWS Secrets Manager) that enforces per-secret access policies.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for General Skills

What to try next