Klodd


Klodd is a service that makes it easy to deploy per-team instances for CTF challenges. Though the service is great, the documentation for new CTF developers is less than ideal.

Some CTF challenges, particularly those involving prototype pollution and remote code execution, can potentially allow competitors to interfere with each other, whether out of malice or simply by solving the challenge. In these situations, it can be difficult to deploy challenges in a way that keeps CTFs fun and competitive for everyone. Klodd solves this problem, by giving each team their own deployment that is identical to everyone else’s.

You are responsible for properly configuring a wildcard DNS record to point these subdomains at your cluster. Ensure that TLS is properly configured in Traefik as well.

Authentiation

Just a side note, Klodd Auth can be configured through rCTF. View more information about this here: Klodd Auth. I would reccomend using rCTF auth, as it worked seamlessly for me.

To set up the rCTF OAuth required by Klodd, you’ll need to configure a /auth route that redirects incoming requests to the Klodd auth endpoint and injects the users rCTF token as described by their docs here.

There’s no reason to require a Cloudflare worker though; on our custom frontend, we implemented this with a Next.js route handler with similar logic.

Installation

Install Docker, k3s and kubectl

Docker Registry

Your challenge docker images must be hosted on a docker registry. This can be done through Docker Hub or by hosting your own registry with docker run -d -p 5000:5000 --name registry registry:2.7

The exposed port can be whatever you want, just make sure to pull from that port in the future.

Build your image with docker build -t localhost:5000/image-name:tag . Then, push it to the registry with docker push localhost:5000/image-name:tag

Traefik

Klodd depends on Traefik as a ingress controller. Though you can configure Traefik yourself, it is easier to use the default Helm chart.

To install the Traefik with Helm, run the following commands:

  1. Ensure you have Helm v3 > 3.9.0 installed.
  2. Add the Traefik Helm repository: helm repo add traefik https://traefik.github.io/charts
  3. Deploy Traefik: helm install traefik traefik/traefik

If you wish to restart Traefik, you can run helm uninstall traefik to stop the service, and then helm install traefik traefik/traefik.

If you wish to run Traefik with custom values you can use helm install traefik traefik/traefik -f values.yaml.

Install the Klodd CRDs and ClusterRole

Use the following commands to install the Klodd CRDs and ClusterRole.

# Install Klodd Challenge CRD
kubectl apply -f https://raw.githubusercontent.com/TJCSec/klodd/master/manifests/klodd-crd.yaml

# Install Klodd ClusterRole
kubectl apply -f https://raw.githubusercontent.com/TJCSec/klodd/master/manifests/klodd-rbac.yaml

Starting Klodd

In yaml files listed below, you only need to worry about fields with comments.

Place the following in a file named workloads.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: klodd
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: klodd
  namespace: klodd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: klodd
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: klodd # 
subjects:
  - kind: ServiceAccount
    name: klodd
    namespace: klodd
---
apiVersion: v1
kind: Secret
metadata:
  name: klodd
  namespace: klodd
type: Opaque
data: # This is customizable. Base64 decode it or view the sample config.yaml below
  config.yaml: Y2hhbGxlbmdlRG9tYWluOiBsb2NhbGhvc3QuZGlyZWN0Cmt1YmVDb25maWc6IGNsdXN0ZXIKcHVibGljVXJsOiBodHRwczovL2tsb2RkLmxvY2FsaG9zdC5kaXJlY3QKcmN0ZlVybDogaHR0cHM6Ly9iMDFsZXJzYy50Zgp0cmFlZmlrOiAKICBodHRwRW50cnlwb2ludDogd2Vic2VjdXJlCiAgdGNwRW50cnlwb2ludDogdGNwCiAgdGNwUG9ydDogMTMzNwppbmdyZXNzOiAKICBuYW1lc3BhY2VTZWxlY3RvcjoKICAgIG1hdGNoTGFiZWxzOgogICAgICBrdWJlcm5ldGVzLmlvL21ldGFkYXRhLm5hbWU6IGRlZmF1bHQgI2Vuc3VyZSB0aGlzIG1hdGNoZXMgdGhlIG5hbWVzcGFjZSB0cmFlZmlrIGlzIG9uLgogIHBvZFNlbGVjdG9yOgogICAgbWF0Y2hMYWJlbHM6CiAgICAgIGFwcC5rdWJlcm5ldGVzLmlvL25hbWU6IHRyYWVmaWsKc2VjcmV0S2V5OiAicmFuZG9tbHkgZ2VuZXJhdGVkIHNlY3JldCBrZXkiCnJlY2FwdGNoYToKICBzaXRlS2V5OiA2TGVJeEFjVEFBQUFBSmNaVlJxeUhoNzFVTUlFR05RX01YamlaS2hJCiAgc2VjcmV0S2V5OiA2TGVJeEFjVEFBQUFBR0ctdkZJMVRuUld4TVpORnVvako0V2lmSldl
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: klodd
  namespace: klodd
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: klodd
  template:
    metadata:
      labels:
        app.kubernetes.io/name: klodd
    spec:
      serviceAccountName: klodd # 
      volumes:
        - name: config
          secret:
            secretName: klodd
      containers:
        - name: klodd
          image: ghcr.io/tjcsec/klodd:master # 
          volumeMounts:
            - name: config
              mountPath: /app/config/
              readOnly: true
          ports:
            - name: public
              containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
  name: klodd
  namespace: klodd
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: klodd
  ports:
    - name: public
      port: 5000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: klodd
  namespace: klodd
spec:
  rules:
    - host: klodd.localhost.direct #change this eventually to your public domain
      http:
        paths:
          - backend:
              service:
                name: klodd
                port:
                  number: 5000
            path: /
            pathType: ImplementationSpecific

View the example base64 decoded config.yaml below:

challengeDomain: localhost.direct # use localhost.direct for local testing, or your domain for production
kubeConfig: cluster
publicUrl: https://klodd.localhost.direct # eventually change this to your domain
rctfUrl: https://b01lersc.tf # Your rCTF URL here.
traefik: 
  httpEntrypoint: websecure
  tcpEntrypoint: tcp
  tcpPort: 1337
ingress: 
  namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: default #ensure this matches the namespace traefik is on.
  podSelector:
    matchLabels:
      app.kubernetes.io/name: traefik
secretKey: "randomly generated secret key"
recaptcha: # These are test keys. Replace them with your own keys when moving to production.
  siteKey: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
  secretKey: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

To change the config.yaml file, base64 encode it and replace the config.yaml value in the workloads.yaml file.

For local testing, you should only really need to chance the rctfUrl. The challengeDomain should be localhost.direct and the publicUrl should be https://klodd.localhost.direct.

To deploy Klodd, run the following command:

kubectl apply -f workloads.yaml

Verify that Klodd is running by running the following:

  1. kubectl get namespace
  2. Ensure the klodd namespace is present. You should see something like the following:
    NAME              STATUS   AGE
    default           Active   31h
    klodd             Active   5s
    kube-node-lease   Active   31h
    kube-public       Active   31h
    kube-system       Active   31h
    

At this point, you should be able to go to https://klodd.localhost.direct/ and see the Klodd frontend.

Klodd Frontend

Building a Challenge

If you haven’t already, build your image with docker build -t localhost:5000/image-name:tag . Then, push it to the registry with docker push localhost:5000/image-name:tag.

Replace the port with the one you exposed earlier if you decided not to use 5000.

Create a file named challenge.yaml with the following contents:

apiVersion: "klodd.tjcsec.club/v1"
kind: Challenge
metadata:
  name: test # The name of the resource is also used in the challenge URL. For example, the page for this challenge is accessible at /challenge/test.
spec:
  name: Test Challenge # This is the name displayed on the frontend. It does not have to be related to metadata.name in any way.
  timeout: 10000 # Each instance will be stopped after this many milliseconds.
  pods:
    - name: app # the name of the pod, ensure this and expose.pod match
      ports: 
        - port: 80 # listed port inside the container, ensure this matches the exposed port
      spec:
        containers:
          - name: main
            image: localhost:5000/image-name:tag # The image to run for this pod.
            resources:
              requests:
                memory: 100Mi
                cpu: 75m
              limits:
                memory: 250Mi
                cpu: 100m
        automountServiceAccountToken: false
  expose:
    kind: http
    pod: app # the name of the pod, ensure this and pods.name match
    port: 80 # the port to expose, ensure this matches the listed port in pods.ports.port
  middlewares:
    - contentType:
        autoDetect: false
    - rateLimit:
        average: 5
        burst: 10

To deploy the challenge, run the following command:

kubectl create -f challenge.yaml

You should see something like

challenge.klodd.tjcsec.club/test created

Ensure your challenge is running properly with the following:

  1. kubectl get challenge
  2. Ensure the challenge is present. You should see something like the following:
    NAME  STATUS   AGE
    test  Active   5s
    

You can delete your challenge with kubectl delete challenge test

Now, you should be able to go to https://klodd.localhost.direct/challenge/test and see your challenge.

Congratulations! You have successfully deployed a challenge with Klodd!

More examples of Klodd deployments can be found here: Klodd Examples

Common Issues

Challenge Not Running

If you click the run button and your challenge stays in the “starting” state, there is probably an issue locating your docker image. Ensure that your image is built and pushed to the registry correctly, and that you are using the correct image name and tag in your challenge.yaml file.

You can also check traefik/whoami:latest as a test image to see if it is a problem with the image.

This is on port 80.

Unknown as the challenge status

If you see “unknown” as the status of your challenge, there is probably a port conflict or matching issue. Ensure you are using the same two ports in your challenge.yaml file, and that it matches with your exposed port in the docker image.

Debugging

You can view the traefik container’s logs to see specific errors, or to find the namespace of the problematic pod.