From 199584241574ae7096f3649b1efacf3061bdf9eb Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:06:15 +0100 Subject: [PATCH 1/9] feat: add support for generating shared challenges with shared deployment templates --- README.md | 5 +- src/commands/challenge_creator.py | 2 +- src/library/generator.py | 57 ++++++++++++-- template/shared-tcp-k8s.yml | 105 ++++++++++++++++++++++++++ template/shared-web-k8s.yml | 121 ++++++++++++++++++++++++++++++ 5 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 template/shared-tcp-k8s.yml create mode 100644 template/shared-web-k8s.yml diff --git a/README.md b/README.md index 66bbae2..da9c98a 100644 --- a/README.md +++ b/README.md @@ -577,6 +577,8 @@ The following templates are required: - Challenge deployment templates: - Web: `instanced-web-k8s.yml` - TCP: `instanced-tcp-k8s.yml` + - Web: `shared-web-k8s.yml` + - TCP: `shared-tcp-k8s.yml` - [kube-ctf](https://github.com/ctfpilot/kube-ctf) deployment template: - `instanced-k8s-challenge.yml` @@ -584,9 +586,6 @@ The following templates are required: **Challenge deployment templates** are used to generate the Kubernetes deployment files for challenges. The **`kube-ctf` deployment template** is used to generate the deployment file for instanced challenges, when using the [kube-ctf](https://github.com/ctfpilot/kube-ctf) platform. Within this template, the challenge deployment template is embedded. -> [!NOTE] -> Only instanced templates are currently generated. Shared templates are not yet supported. - ### Page structure > [!NOTE] diff --git a/src/commands/challenge_creator.py b/src/commands/challenge_creator.py index 0f04166..efa06c1 100644 --- a/src/commands/challenge_creator.py +++ b/src/commands/challenge_creator.py @@ -147,7 +147,7 @@ def prompt(self, challenge: Challenge): else: challenge.set_min_points(args.min_points) - if (args.type == "instanced" or prompted_type == "instanced") and args.instanced_type == "none": + if ((args.type == "instanced" or prompted_type == "instanced") or (args.type == "shared" or prompted_type == "shared")) and args.instanced_type == "none": while True: try: challenge.set_instanced_type(input(f"Type of instanced challenge ({', '.join(INSTANCED_TYPES)}): ").lower()) diff --git a/src/library/generator.py b/src/library/generator.py index 54a8269..61ed000 100644 --- a/src/library/generator.py +++ b/src/library/generator.py @@ -60,7 +60,12 @@ def build(self): self.dockerfile() - self.instanced_template_file() + if self.challenge.type == "shared": + self.shared_template_file() + elif self.challenge.type == "instanced": + self.instanced_template_file() + else: + print(f"Challenge type {self.challenge.type} not supported for template generation.") # --- Helper functions --- @@ -266,16 +271,16 @@ def instanced_template_file_exists(self): return self.check_if_dir_exists(os.path.join(self.dir_template, "k8s.yml")) def instanced_template_file(self): - # Check if needed template exists - if not self.instanced_template_source_file_exists(): - print("k8s template files not found!") - return False - # Check if template directory to write to exists if not self.check_if_dir_exists(self.dir_template): print("Template directory not found!") return False + # Check if needed template exists + if not self.instanced_template_source_file_exists(): + print("Instanced k8s template files not found!") + return False + # Check if template file already exists if self.instanced_template_file_exists(): print("Template file already exists!") @@ -297,6 +302,46 @@ def instanced_template_file(self): print(f"File created: {output_file}") return True + + def shared_template_source_file_exists(self): + return self.check_if_dir_exists(os.path.join(Utils.get_template_dir(), "shared-web-k8s.yml")) and \ + self.check_if_dir_exists(os.path.join(Utils.get_template_dir(), "shared-tcp-k8s.yml")) + + def shared_template_file_exists(self): + return self.check_if_dir_exists(os.path.join(self.dir_template, "k8s.yml")) + + def shared_template_file(self): + # Check if template directory to write to exists + if not self.check_if_dir_exists(self.dir_template): + print("Template directory not found!") + return False + + # Check if needed template exists + if not self.shared_template_source_file_exists(): + print("Shared k8s template files not found!") + return False + + # Check if template file already exists + if self.shared_template_file_exists(): + print("Template file already exists!") + return False + + if self.challenge.instanced_type == "web": + source_file = os.path.join(Utils.get_template_dir(), "shared-web-k8s.yml") + elif self.challenge.instanced_type == "tcp": + source_file = os.path.join(Utils.get_template_dir(), "shared-tcp-k8s.yml") + else: + print(f"Instanced type {self.challenge.instanced_type} is not supported for shared challenges.") + return False + + output_file = os.path.join(self.dir_template, "k8s.yml") + with open(source_file, "r") as f: + with open(output_file, "w") as of: + of.write(f.read()) + + print(f"File created: {output_file}") + + return True def version_file_exists(self): return self.check_if_dir_exists(os.path.join(self.path, "version")) diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml new file mode 100644 index 0000000..476db0a --- /dev/null +++ b/template/shared-tcp-k8s.yml @@ -0,0 +1,105 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + namespace: kubectf-challenges + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" +spec: + replicas: 1 + selector: + matchLabels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + template: + metadata: + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" + spec: + enableServiceLinks: false + automountServiceAccountToken: false + imagePullSecrets: + - name: dockerconfigjson-github-com + dnsPolicy: None + dnsConfig: + nameservers: + - 1.1.1.1 + - 8.8.8.8 + tolerations: + - key: "cluster.ctfpilot.com/node" + value: "scaler" + effect: "PreferNoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "cluster.ctfpilot.com/node" + operator: In + values: + - scaler + containers: + - name: web + image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }} + imagePullPolicy: IfNotPresent + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 10m + memory: 32Mi + ports: + - containerPort: 8080 + name: tcp +--- +apiVersion: v1 +kind: Service +metadata: + name: "ctf-{{ deployment_id }}" + namespace: kubectf-challenges + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" +spec: + selector: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ports: + - port: 8080 + name: tcp +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteTCP +metadata: + name: "chall-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + namespace: kubectf-challenges + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" + annotations: + traefik.ingress.kubernetes.io/router.priority: "100" +spec: + entryPoints: + - tcp-test # Needs to be a custom entrypoint defined in Traefik - This is defined per challenge + routes: + - match: HostSNI(`*`) + priority: 10 + middlewares: + - name: challenge-ipwhitelist-tcp + namespace: kubectf-challenges + services: + - name: "chall-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + port: 8080 diff --git a/template/shared-web-k8s.yml b/template/shared-web-k8s.yml new file mode 100644 index 0000000..387a0f8 --- /dev/null +++ b/template/shared-web-k8s.yml @@ -0,0 +1,121 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + namespace: kubectf-challenges + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" +spec: + replicas: 1 + selector: + matchLabels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + template: + metadata: + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" + spec: + enableServiceLinks: false + automountServiceAccountToken: false + imagePullSecrets: + - name: dockerconfigjson-github-com + dnsPolicy: None + dnsConfig: + nameservers: + - 1.1.1.1 + - 8.8.8.8 + tolerations: + - key: "cluster.ctfpilot.com/node" + value: "scaler" + effect: "PreferNoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "cluster.ctfpilot.com/node" + operator: In + values: + - scaler + containers: + - name: web + image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }} + imagePullPolicy: IfNotPresent + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 10m + memory: 32Mi + ports: + - containerPort: 80 + name: web-port + startupProbe: + httpGet: + path: / + port: web-port + failureThreshold: 12 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: web-port + initialDelaySeconds: 5 + timeoutSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + namespace: kubectf-challenges + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" +spec: + selector: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ports: + - port: 80 + targetPort: web-port +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: "ingress-ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + namespace: kubectf-challenges-shared + labels: + challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" + challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" + challenges.ctfpilot.com/category: "{{ CHALLENGE_CATEGORY }}" + ctfpilot.com/component: "shared-challenge" + annotations: + traefik.ingress.kubernetes.io/router.middlewares: kubectf-instancing-fallback@kubernetescrd +spec: + tls: + - hosts: + - "{{ CHALLENGE_NAME }}.{{ .Values.kubectf.host }}" + secretName: kubectf-cert-challs + rules: + - host: "{{ CHALLENGE_NAME }}.{{ .Values.kubectf.host }}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }} + port: + number: 80 From 8d01a5893820ba990722f8e7fe4171e4c5291976 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:11:48 +0100 Subject: [PATCH 2/9] refactor: update service and ingress names to include challenge category and name --- template/shared-tcp-k8s.yml | 6 +++--- template/shared-web-k8s.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml index 476db0a..864c7da 100644 --- a/template/shared-tcp-k8s.yml +++ b/template/shared-tcp-k8s.yml @@ -63,7 +63,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: "ctf-{{ deployment_id }}" + name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" namespace: kubectf-challenges labels: challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" @@ -82,7 +82,7 @@ spec: apiVersion: traefik.io/v1alpha1 kind: IngressRouteTCP metadata: - name: "chall-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + name: "ingress-ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" namespace: kubectf-challenges labels: challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" @@ -101,5 +101,5 @@ spec: - name: challenge-ipwhitelist-tcp namespace: kubectf-challenges services: - - name: "chall-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" + - name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" port: 8080 diff --git a/template/shared-web-k8s.yml b/template/shared-web-k8s.yml index 387a0f8..cf1df92 100644 --- a/template/shared-web-k8s.yml +++ b/template/shared-web-k8s.yml @@ -95,7 +95,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: "ingress-ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" - namespace: kubectf-challenges-shared + namespace: kubectf-challenges labels: challenges.ctfpilot.com/type: "{{ CHALLENGE_TYPE }}" challenges.ctfpilot.com/name: "{{ CHALLENGE_NAME }}" From 036e746762ee677210b3c13ad547a9aab9796885 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:16:15 +0100 Subject: [PATCH 3/9] docs: clarify challenge deployment templates in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index da9c98a..9a7b52c 100644 --- a/README.md +++ b/README.md @@ -575,10 +575,10 @@ The following templates are required: - `challenge-configmap.yml` - `page-configmap.yml` - Challenge deployment templates: - - Web: `instanced-web-k8s.yml` - - TCP: `instanced-tcp-k8s.yml` - - Web: `shared-web-k8s.yml` - - TCP: `shared-tcp-k8s.yml` + - Instanced web: `instanced-web-k8s.yml` + - Instanced TCP: `instanced-tcp-k8s.yml` + - Shared Web: `shared-web-k8s.yml` + - Shared TCP: `shared-tcp-k8s.yml` - [kube-ctf](https://github.com/ctfpilot/kube-ctf) deployment template: - `instanced-k8s-challenge.yml` From 81e8cdad8f4f006f738b7f5258261f75944d6da5 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:17:37 +0100 Subject: [PATCH 4/9] fix: rename container name from 'web' to 'tcp' in TCP deployment templates --- template/instanced-tcp-k8s.yml | 2 +- template/shared-tcp-k8s.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/template/instanced-tcp-k8s.yml b/template/instanced-tcp-k8s.yml index 8d050c3..23c4885 100644 --- a/template/instanced-tcp-k8s.yml +++ b/template/instanced-tcp-k8s.yml @@ -52,7 +52,7 @@ spec: values: - scaler containers: - - name: web + - name: tcp image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }} imagePullPolicy: IfNotPresent resources: diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml index 864c7da..9378b3b 100644 --- a/template/shared-tcp-k8s.yml +++ b/template/shared-tcp-k8s.yml @@ -46,7 +46,7 @@ spec: values: - scaler containers: - - name: web + - name: tcp image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }} imagePullPolicy: IfNotPresent resources: From ee4ce946b3c7a454f2ed1b926a83ad7399c03b53 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:20:10 +0100 Subject: [PATCH 5/9] refactor: improve instanced type prompt for challenge creation --- src/commands/challenge_creator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/challenge_creator.py b/src/commands/challenge_creator.py index efa06c1..d1e193e 100644 --- a/src/commands/challenge_creator.py +++ b/src/commands/challenge_creator.py @@ -147,10 +147,10 @@ def prompt(self, challenge: Challenge): else: challenge.set_min_points(args.min_points) - if ((args.type == "instanced" or prompted_type == "instanced") or (args.type == "shared" or prompted_type == "shared")) and args.instanced_type == "none": + if (args.type in [ "instanced", "shared" ] or prompted_type in [ "instanced", "shared" ]) and args.instanced_type == "none": while True: try: - challenge.set_instanced_type(input(f"Type of instanced challenge ({', '.join(INSTANCED_TYPES)}): ").lower()) + challenge.set_instanced_type(input(f"Instanced type for challenge ({', '.join(INSTANCED_TYPES)}): ").lower()) break except ValueError: print("Invalid instanced type. Please try again.") From a07ec69aa27ca17404c061ae331b49c46d103b52 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:28:51 +0100 Subject: [PATCH 6/9] refactor: add targetPort to TCP service and correct service name formatting in web service --- template/shared-tcp-k8s.yml | 1 + template/shared-web-k8s.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml index 9378b3b..324be64 100644 --- a/template/shared-tcp-k8s.yml +++ b/template/shared-tcp-k8s.yml @@ -78,6 +78,7 @@ spec: ports: - port: 8080 name: tcp + targetPort: tcp --- apiVersion: traefik.io/v1alpha1 kind: IngressRouteTCP diff --git a/template/shared-web-k8s.yml b/template/shared-web-k8s.yml index cf1df92..f64c276 100644 --- a/template/shared-web-k8s.yml +++ b/template/shared-web-k8s.yml @@ -116,6 +116,6 @@ spec: pathType: Prefix backend: service: - name: ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }} + name: "ctf-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }}" port: number: 80 From ffd98f2a5507c8f241d5c632567aff1a676d91af Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:29:23 +0100 Subject: [PATCH 7/9] fix: add targetPort to TCP service in instanced deployment template --- template/instanced-tcp-k8s.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/template/instanced-tcp-k8s.yml b/template/instanced-tcp-k8s.yml index 23c4885..03c4634 100644 --- a/template/instanced-tcp-k8s.yml +++ b/template/instanced-tcp-k8s.yml @@ -87,6 +87,7 @@ spec: ports: - port: 8080 name: tcp + targetPort: tcp --- apiVersion: traefik.io/v1alpha1 kind: IngressRouteTCP From b4313a5798260d3d1db3b5a199ae60f351176d25 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:30:48 +0100 Subject: [PATCH 8/9] fix: set instanced type for challenge based on user input --- src/commands/challenge_creator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/challenge_creator.py b/src/commands/challenge_creator.py index d1e193e..5ed25b9 100644 --- a/src/commands/challenge_creator.py +++ b/src/commands/challenge_creator.py @@ -154,6 +154,8 @@ def prompt(self, challenge: Challenge): break except ValueError: print("Invalid instanced type. Please try again.") + elif args.instanced_type != "none": + challenge.set_instanced_type(args.instanced_type) else: challenge.set_instanced_type("none") From 62a9c1e4a1e48537e51a586cbdb668be449593fc Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 15:31:47 +0100 Subject: [PATCH 9/9] refactor: update TCP entry point to use challenge category and name placeholders --- template/shared-tcp-k8s.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml index 324be64..f7c920e 100644 --- a/template/shared-tcp-k8s.yml +++ b/template/shared-tcp-k8s.yml @@ -94,7 +94,7 @@ metadata: traefik.ingress.kubernetes.io/router.priority: "100" spec: entryPoints: - - tcp-test # Needs to be a custom entrypoint defined in Traefik - This is defined per challenge + - tcp-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }} # Needs to be a custom entrypoint defined in Traefik - This is defined per challenge routes: - match: HostSNI(`*`) priority: 10