Skip to content

Commit 45b84fc

Browse files
sroettgerstephenR
andauthored
Support for additional domains (#311)
* new domains: option to create additional certificates * add docs how to use vanity domains * Update kctf.dev_challenges_crd.yaml * Update functions.go * Automated commit: update images. Co-authored-by: Stephen Roettger <[email protected]>
1 parent 38a1bbb commit 45b84fc

File tree

9 files changed

+167
-27
lines changed

9 files changed

+167
-27
lines changed

dist/resources/kctf.dev_challenges_crd.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ spec:
120120
- type: string
121121
description: TargetPort is not optional
122122
x-kubernetes-int-or-string: true
123+
domains:
124+
description: Extra domains to get certificates for. Only used for protocol HTTPS.
125+
You need set up DNS manually by adding a CNAME entry from domain to chal-web.ctf.tld.
126+
items:
127+
type: string
128+
type: array
123129
required:
124130
- protocol
125131
- targetPort

dist/resources/operator.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ spec:
1616
serviceAccountName: kctf-operator
1717
containers:
1818
- name: kctf-operator
19-
image: gcr.io/kctf-docker/kctf-operator@sha256:d049545f2a0a23e37eede433800161831b7ec61c3cc8309ed4b4ec24124df47d
19+
image: gcr.io/kctf-docker/kctf-operator@sha256:5d8dea60ae41bb6b1834d092ffbfeddb94758dd73c566c511d8c8d9c9b9f884a
2020
command:
2121
- kctf-operator
2222
imagePullPolicy: Always

docs/custom-domains.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Custom Domains
2+
3+
When creating your cluster, you can specify a domain with the `--domain-name` flag.
4+
kCTF will then automatically create domain names for challenges of the form:
5+
* $chal\_name.$kctf\_domain for TCP based challenges
6+
* $chal\_name-web.$kctf\_domain for HTTPS based challenges
7+
8+
You might want to use custom domains for some of your challenges, for example:
9+
* if you need to have a challenge available on multiple host names
10+
* to protect web challenges against same-site attacks
11+
* or simply if you want to have a fancy domain name
12+
13+
For TCP based challenges, all you need to do is to create a CNAME DNS entry from $cooldomain to $chal\_name.$kctf\_domain.
14+
15+
For HTTPS based challenges, you also need to add a CNAME entry (pay attention to the -web suffix) and in addition, list the domain in the port configuration of the challenge:
16+
```yaml
17+
apiVersion: kctf.dev/v1
18+
kind: Challenge
19+
metadata:
20+
name: web
21+
spec:
22+
deployed: true
23+
powDifficultySeconds: 0
24+
network:
25+
public: true
26+
ports:
27+
- protocol: "HTTPS"
28+
targetPort: 1337
29+
domains:
30+
- "cooldomain.com"
31+
```
32+
With this, kCTF will automatically create a certificate for you and attach it to the challenge's LoadBalancer.

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ If you are able to break out of it, you can [earn up to $10,000 USD](vrp.md).
3838

3939
* [Local Testing Walkthrough](local-testing.md) – A quick start guide showing you how to build and test challenges locally.
4040
* [kCTF in 8 Minutes](introduction.md) – A quick 8-minute summary of what kCTF is and how it interacts with Kubernetes.
41-
* [Google Cloud Walkthrough](google-cloud.md) – Once you have everything up and running, try deploying to Google Cloud.
41+
* [Google Cloud Walkthrough](google-cloud.md) – Once you have everything up and running, try deploying to Google Cloud.
42+
* [Custom Domains](custom-domains.md) – How to add custom domains for your challenges.
4243
* [Troubleshooting](troubleshooting.md) – Help with fixing broken challenges.
4344
* [CTF playbook](ctf-playbook.md) – How to set up your cluster and challenges to scale during a CTF.
4445
* [Security Threat Model](security-threat-model.md) – Security considerations regarding kCTF including information on assets, risks, and potential attackers.

kctf-operator/deploy/crds/kctf.dev_challenges_crd.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ spec:
120120
- type: string
121121
description: TargetPort is not optional
122122
x-kubernetes-int-or-string: true
123+
domains:
124+
description: Extra domains to get certificates for. Only used for protocol HTTPS.
125+
You need to set up DNS manually by adding a CNAME entry from domain to chal-web.ctf.tld.
126+
items:
127+
type: string
128+
type: array
123129
required:
124130
- protocol
125131
- targetPort

kctf-operator/pkg/apis/kctf/v1/challenge_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type PortSpec struct {
2323
// Protocol is not optional
2424
// +kubebuilder:validation:Required
2525
Protocol corev1.Protocol `json:"protocol"`
26+
27+
// Extra domains for managed certificates. Only used for type HTTPS.
28+
Domains []string `json:"domains,omitempty"`
2629
}
2730

2831
// Network specifications for the service

kctf-operator/pkg/apis/kctf/v1/zz_generated.deepcopy.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kctf-operator/pkg/controller/challenge/service/functions.go

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"reflect"
99

10+
gkenetv1 "github.com/GoogleCloudPlatform/gke-managed-certs/pkg/apis/networking.gke.io/v1"
1011
"github.com/go-logr/logr"
1112
backendv1 "github.com/google/kctf/pkg/apis/cloud/v1"
1213
kctfv1 "github.com/google/kctf/pkg/apis/kctf/v1"
@@ -27,6 +28,10 @@ func isServiceEqual(serviceFound *corev1.Service, serv *corev1.Service) bool {
2728
return reflect.DeepEqual(serviceFound.Spec.LoadBalancerSourceRanges, serv.Spec.LoadBalancerSourceRanges)
2829
}
2930

31+
func isCertEqual(existingCert *gkenetv1.ManagedCertificate, newCert *gkenetv1.ManagedCertificate) bool {
32+
return reflect.DeepEqual(existingCert.Spec.Domains, newCert.Spec.Domains)
33+
}
34+
3035
func isIngressEqual(ingressFound *netv1beta1.Ingress, ingress *netv1beta1.Ingress) bool {
3136
return reflect.DeepEqual(ingressFound.Spec, ingress.Spec)
3237
}
@@ -125,6 +130,47 @@ func updateBackendConfig(challenge *kctfv1.Challenge, client client.Client, sche
125130
return true, err
126131
}
127132

133+
func updateManagedCertificate(challenge *kctfv1.Challenge, client client.Client, scheme *runtime.Scheme,
134+
log logr.Logger, ctx context.Context) (bool, error) {
135+
136+
existingCert := &gkenetv1.ManagedCertificate{}
137+
err := client.Get(ctx, types.NamespacedName{Name: challenge.Name, Namespace: challenge.Namespace}, existingCert)
138+
139+
if err != nil && !errors.IsNotFound(err) {
140+
return false, err
141+
}
142+
certExists := err == nil
143+
144+
port := findHTTPSPort(challenge)
145+
if port == nil || port.Domains == nil {
146+
if certExists {
147+
err := client.Delete(ctx, existingCert)
148+
return true, err
149+
}
150+
return false, nil
151+
}
152+
153+
newCert := generateManagedCertificate(challenge, port.Domains)
154+
155+
if certExists {
156+
if isCertEqual(existingCert, newCert) {
157+
return false, nil
158+
}
159+
160+
existingCert.Spec.Domains = newCert.Spec.Domains
161+
162+
err := client.Update(ctx, existingCert)
163+
164+
return true, err
165+
}
166+
167+
controllerutil.SetControllerReference(challenge, newCert, scheme)
168+
169+
err = client.Create(ctx, newCert)
170+
171+
return true, err
172+
}
173+
128174
func updateIngress(challenge *kctfv1.Challenge, client client.Client, scheme *runtime.Scheme,
129175
log logr.Logger, ctx context.Context) (bool, error) {
130176
existingIngress := &netv1beta1.Ingress{}
@@ -135,15 +181,22 @@ func updateIngress(challenge *kctfv1.Challenge, client client.Client, scheme *ru
135181
}
136182
ingressExists := err == nil
137183

138-
domainName := utils.GetDomainName(challenge, client, log, ctx)
139-
newIngress := generateIngress(domainName, challenge)
140-
141-
if ingressExists {
142-
if newIngress.Spec.Backend == nil || challenge.Spec.Network.Public == false {
184+
port := findHTTPSPort(challenge)
185+
// Only one https port is supported at the moment.
186+
// To support more, we will need a field to specify the domain name per ingress.
187+
188+
if port == nil {
189+
if ingressExists {
143190
err := client.Delete(ctx, existingIngress)
144191
return true, err
145192
}
193+
return false, nil
194+
}
146195

196+
domainName := utils.GetDomainName(challenge, client, log, ctx)
197+
newIngress := generateIngress(domainName, challenge, port)
198+
199+
if ingressExists {
147200
if isIngressEqual(existingIngress, newIngress) {
148201
return false, nil
149202
}
@@ -280,6 +333,14 @@ func Update(challenge *kctfv1.Challenge, client client.Client, scheme *runtime.S
280333
}
281334
changed = changed || backendConfigChanged
282335

336+
managedCertificateChanged, err := updateManagedCertificate(challenge, client, scheme, log, ctx)
337+
if err != nil {
338+
log.Error(err, "Error updating ManagedCertificate", " Name: ",
339+
challenge.Name, " with namespace ", challenge.Namespace)
340+
return false, err
341+
}
342+
changed = changed || managedCertificateChanged
343+
283344
ingressChanged, err := updateIngress(challenge, client, scheme, log, ctx)
284345
if err != nil {
285346
log.Error(err, "Error updating ingress", " Name: ",

kctf-operator/pkg/controller/challenge/service/service.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strconv"
77
"strings"
88

9+
gkenetv1 "github.com/GoogleCloudPlatform/gke-managed-certs/pkg/apis/networking.gke.io/v1"
910
backendv1 "github.com/google/kctf/pkg/apis/cloud/v1"
1011
kctfv1 "github.com/google/kctf/pkg/apis/kctf/v1"
1112
corev1 "k8s.io/api/core/v1"
@@ -79,14 +80,43 @@ func generateBackendConfig(challenge *kctfv1.Challenge) *backendv1.BackendConfig
7980
return config
8081
}
8182

82-
func generateIngress(domainName string, challenge *kctfv1.Challenge) *netv1beta1.Ingress {
83-
// Ingress object
84-
ingress := &netv1beta1.Ingress{
83+
func findHTTPSPort(challenge *kctfv1.Challenge) *kctfv1.PortSpec {
84+
for _, port := range challenge.Spec.Network.Ports {
85+
// non-HTTPS is handled by generateLoadBalancerService
86+
if port.Protocol != "HTTPS" {
87+
continue
88+
}
89+
return &port
90+
}
91+
return nil
92+
}
93+
94+
func generateManagedCertificate(challenge *kctfv1.Challenge, domains []string) *gkenetv1.ManagedCertificate {
95+
cert := &gkenetv1.ManagedCertificate{
8596
ObjectMeta: metav1.ObjectMeta{
8697
Name: challenge.Name,
8798
Namespace: challenge.Namespace,
8899
Labels: map[string]string{"app": challenge.Name},
89100
},
101+
Spec: gkenetv1.ManagedCertificateSpec{
102+
Domains: domains,
103+
},
104+
Status: gkenetv1.ManagedCertificateStatus{
105+
DomainStatus: []gkenetv1.DomainStatus{},
106+
},
107+
}
108+
return cert
109+
}
110+
111+
func generateIngress(domainName string, challenge *kctfv1.Challenge, port *kctfv1.PortSpec) *netv1beta1.Ingress {
112+
// Ingress object
113+
ingress := &netv1beta1.Ingress{
114+
ObjectMeta: metav1.ObjectMeta{
115+
Name: challenge.Name,
116+
Namespace: challenge.Namespace,
117+
Labels: map[string]string{"app": challenge.Name},
118+
Annotations: map[string]string{},
119+
},
90120
Spec: netv1beta1.IngressSpec{
91121
TLS: []netv1beta1.IngressTLS{{
92122
SecretName: "tls-cert",
@@ -97,24 +127,18 @@ func generateIngress(domainName string, challenge *kctfv1.Challenge) *netv1beta1
97127
},
98128
}
99129

100-
for _, port := range challenge.Spec.Network.Ports {
101-
// non-HTTPS is handled by generateLoadBalancerService
102-
if port.Protocol != "HTTPS" {
103-
continue
104-
}
130+
servicePort := port.Port
131+
if servicePort == 0 {
132+
servicePort = port.TargetPort.IntVal
133+
}
105134

106-
servicePort := port.Port
107-
if servicePort == 0 {
108-
servicePort = port.TargetPort.IntVal
109-
}
135+
ingress.Spec.Backend = &netv1beta1.IngressBackend{
136+
ServiceName: challenge.Name,
137+
ServicePort: intstr.FromInt(int(servicePort)),
138+
}
110139

111-
ingress.Spec.Backend = &netv1beta1.IngressBackend{
112-
ServiceName: challenge.Name,
113-
ServicePort: intstr.FromInt(int(servicePort)),
114-
}
115-
// Only one https port is supported at the moment.
116-
// To support more, we will need a field to specify the domain name per ingress.
117-
break
140+
if port.Domains != nil {
141+
ingress.Annotations["networking.gke.io/managed-certificates"] = challenge.Name
118142
}
119143

120144
return ingress

0 commit comments

Comments
 (0)