Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,18 +575,17 @@ 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`

**Configmap templates** are used to generate ConfigMaps for challenges and pages.
**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]
Expand Down
6 changes: 4 additions & 2 deletions src/commands/challenge_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
57 changes: 51 additions & 6 deletions src/library/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down Expand Up @@ -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!")
Expand All @@ -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"))
Expand Down
3 changes: 2 additions & 1 deletion template/instanced-tcp-k8s.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ spec:
values:
- scaler
containers:
- name: web
- name: tcp
image: ghcr.io/{{ CHALLENGE_REPO }}-{{ DOCKER_IMAGE }}:{{ CHALLENGE_VERSION }}
imagePullPolicy: IfNotPresent
resources:
Expand Down Expand Up @@ -87,6 +87,7 @@ spec:
ports:
- port: 8080
name: tcp
targetPort: tcp
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
Expand Down
106 changes: 106 additions & 0 deletions template/shared-tcp-k8s.yml
Original file line number Diff line number Diff line change
@@ -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
121 changes: 121 additions & 0 deletions template/shared-web-k8s.yml
Original file line number Diff line number Diff line change
@@ -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