diff --git a/README.md b/README.md index 66bbae2..9a7b52c 100644 --- a/README.md +++ b/README.md @@ -575,8 +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` + - 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` @@ -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..5ed25b9 100644 --- a/src/commands/challenge_creator.py +++ b/src/commands/challenge_creator.py @@ -147,13 +147,15 @@ 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 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.") + elif args.instanced_type != "none": + challenge.set_instanced_type(args.instanced_type) else: challenge.set_instanced_type("none") 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/instanced-tcp-k8s.yml b/template/instanced-tcp-k8s.yml index 8d050c3..03c4634 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: @@ -87,6 +87,7 @@ spec: ports: - port: 8080 name: tcp + targetPort: tcp --- apiVersion: traefik.io/v1alpha1 kind: IngressRouteTCP diff --git a/template/shared-tcp-k8s.yml b/template/shared-tcp-k8s.yml new file mode 100644 index 0000000..f7c920e --- /dev/null +++ b/template/shared-tcp-k8s.yml @@ -0,0 +1,106 @@ +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: tcp + 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-{{ 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: 8080 + name: tcp + targetPort: tcp +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteTCP +metadata: + name: "ingress-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" + annotations: + traefik.ingress.kubernetes.io/router.priority: "100" +spec: + entryPoints: + - tcp-{{ CHALLENGE_CATEGORY }}-{{ CHALLENGE_NAME }} # 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: "ctf-{{ 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..f64c276 --- /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 + 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