Skip to content

Commit

Permalink
Support for additional domains (#311)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
sroettger and stephenR authored Jul 1, 2021
1 parent 38a1bbb commit 45b84fc
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 27 deletions.
6 changes: 6 additions & 0 deletions dist/resources/kctf.dev_challenges_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ spec:
- type: string
description: TargetPort is not optional
x-kubernetes-int-or-string: true
domains:
description: Extra domains to get certificates for. Only used for protocol HTTPS.
You need set up DNS manually by adding a CNAME entry from domain to chal-web.ctf.tld.
items:
type: string
type: array
required:
- protocol
- targetPort
Expand Down
2 changes: 1 addition & 1 deletion dist/resources/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ spec:
serviceAccountName: kctf-operator
containers:
- name: kctf-operator
image: gcr.io/kctf-docker/kctf-operator@sha256:d049545f2a0a23e37eede433800161831b7ec61c3cc8309ed4b4ec24124df47d
image: gcr.io/kctf-docker/kctf-operator@sha256:5d8dea60ae41bb6b1834d092ffbfeddb94758dd73c566c511d8c8d9c9b9f884a
command:
- kctf-operator
imagePullPolicy: Always
Expand Down
32 changes: 32 additions & 0 deletions docs/custom-domains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Custom Domains

When creating your cluster, you can specify a domain with the `--domain-name` flag.
kCTF will then automatically create domain names for challenges of the form:
* $chal\_name.$kctf\_domain for TCP based challenges
* $chal\_name-web.$kctf\_domain for HTTPS based challenges

You might want to use custom domains for some of your challenges, for example:
* if you need to have a challenge available on multiple host names
* to protect web challenges against same-site attacks
* or simply if you want to have a fancy domain name

For TCP based challenges, all you need to do is to create a CNAME DNS entry from $cooldomain to $chal\_name.$kctf\_domain.

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:
```yaml
apiVersion: kctf.dev/v1
kind: Challenge
metadata:
name: web
spec:
deployed: true
powDifficultySeconds: 0
network:
public: true
ports:
- protocol: "HTTPS"
targetPort: 1337
domains:
- "cooldomain.com"
```
With this, kCTF will automatically create a certificate for you and attach it to the challenge's LoadBalancer.
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ If you are able to break out of it, you can [earn up to $10,000 USD](vrp.md).

* [Local Testing Walkthrough](local-testing.md) – A quick start guide showing you how to build and test challenges locally.
* [kCTF in 8 Minutes](introduction.md) – A quick 8-minute summary of what kCTF is and how it interacts with Kubernetes.
* [Google Cloud Walkthrough](google-cloud.md) – Once you have everything up and running, try deploying to Google Cloud.
* [Google Cloud Walkthrough](google-cloud.md) – Once you have everything up and running, try deploying to Google Cloud.
* [Custom Domains](custom-domains.md) – How to add custom domains for your challenges.
* [Troubleshooting](troubleshooting.md) – Help with fixing broken challenges.
* [CTF playbook](ctf-playbook.md) – How to set up your cluster and challenges to scale during a CTF.
* [Security Threat Model](security-threat-model.md) – Security considerations regarding kCTF including information on assets, risks, and potential attackers.
Expand Down
6 changes: 6 additions & 0 deletions kctf-operator/deploy/crds/kctf.dev_challenges_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ spec:
- type: string
description: TargetPort is not optional
x-kubernetes-int-or-string: true
domains:
description: Extra domains to get certificates for. Only used for protocol HTTPS.
You need to set up DNS manually by adding a CNAME entry from domain to chal-web.ctf.tld.
items:
type: string
type: array
required:
- protocol
- targetPort
Expand Down
3 changes: 3 additions & 0 deletions kctf-operator/pkg/apis/kctf/v1/challenge_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type PortSpec struct {
// Protocol is not optional
// +kubebuilder:validation:Required
Protocol corev1.Protocol `json:"protocol"`

// Extra domains for managed certificates. Only used for type HTTPS.
Domains []string `json:"domains,omitempty"`
}

// Network specifications for the service
Expand Down
9 changes: 8 additions & 1 deletion kctf-operator/pkg/apis/kctf/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 66 additions & 5 deletions kctf-operator/pkg/controller/challenge/service/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"reflect"

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

func isCertEqual(existingCert *gkenetv1.ManagedCertificate, newCert *gkenetv1.ManagedCertificate) bool {
return reflect.DeepEqual(existingCert.Spec.Domains, newCert.Spec.Domains)
}

func isIngressEqual(ingressFound *netv1beta1.Ingress, ingress *netv1beta1.Ingress) bool {
return reflect.DeepEqual(ingressFound.Spec, ingress.Spec)
}
Expand Down Expand Up @@ -125,6 +130,47 @@ func updateBackendConfig(challenge *kctfv1.Challenge, client client.Client, sche
return true, err
}

func updateManagedCertificate(challenge *kctfv1.Challenge, client client.Client, scheme *runtime.Scheme,
log logr.Logger, ctx context.Context) (bool, error) {

existingCert := &gkenetv1.ManagedCertificate{}
err := client.Get(ctx, types.NamespacedName{Name: challenge.Name, Namespace: challenge.Namespace}, existingCert)

if err != nil && !errors.IsNotFound(err) {
return false, err
}
certExists := err == nil

port := findHTTPSPort(challenge)
if port == nil || port.Domains == nil {
if certExists {
err := client.Delete(ctx, existingCert)
return true, err
}
return false, nil
}

newCert := generateManagedCertificate(challenge, port.Domains)

if certExists {
if isCertEqual(existingCert, newCert) {
return false, nil
}

existingCert.Spec.Domains = newCert.Spec.Domains

err := client.Update(ctx, existingCert)

return true, err
}

controllerutil.SetControllerReference(challenge, newCert, scheme)

err = client.Create(ctx, newCert)

return true, err
}

func updateIngress(challenge *kctfv1.Challenge, client client.Client, scheme *runtime.Scheme,
log logr.Logger, ctx context.Context) (bool, error) {
existingIngress := &netv1beta1.Ingress{}
Expand All @@ -135,15 +181,22 @@ func updateIngress(challenge *kctfv1.Challenge, client client.Client, scheme *ru
}
ingressExists := err == nil

domainName := utils.GetDomainName(challenge, client, log, ctx)
newIngress := generateIngress(domainName, challenge)

if ingressExists {
if newIngress.Spec.Backend == nil || challenge.Spec.Network.Public == false {
port := findHTTPSPort(challenge)
// Only one https port is supported at the moment.
// To support more, we will need a field to specify the domain name per ingress.

if port == nil {
if ingressExists {
err := client.Delete(ctx, existingIngress)
return true, err
}
return false, nil
}

domainName := utils.GetDomainName(challenge, client, log, ctx)
newIngress := generateIngress(domainName, challenge, port)

if ingressExists {
if isIngressEqual(existingIngress, newIngress) {
return false, nil
}
Expand Down Expand Up @@ -280,6 +333,14 @@ func Update(challenge *kctfv1.Challenge, client client.Client, scheme *runtime.S
}
changed = changed || backendConfigChanged

managedCertificateChanged, err := updateManagedCertificate(challenge, client, scheme, log, ctx)
if err != nil {
log.Error(err, "Error updating ManagedCertificate", " Name: ",
challenge.Name, " with namespace ", challenge.Namespace)
return false, err
}
changed = changed || managedCertificateChanged

ingressChanged, err := updateIngress(challenge, client, scheme, log, ctx)
if err != nil {
log.Error(err, "Error updating ingress", " Name: ",
Expand Down
62 changes: 43 additions & 19 deletions kctf-operator/pkg/controller/challenge/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strconv"
"strings"

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

func generateIngress(domainName string, challenge *kctfv1.Challenge) *netv1beta1.Ingress {
// Ingress object
ingress := &netv1beta1.Ingress{
func findHTTPSPort(challenge *kctfv1.Challenge) *kctfv1.PortSpec {
for _, port := range challenge.Spec.Network.Ports {
// non-HTTPS is handled by generateLoadBalancerService
if port.Protocol != "HTTPS" {
continue
}
return &port
}
return nil
}

func generateManagedCertificate(challenge *kctfv1.Challenge, domains []string) *gkenetv1.ManagedCertificate {
cert := &gkenetv1.ManagedCertificate{
ObjectMeta: metav1.ObjectMeta{
Name: challenge.Name,
Namespace: challenge.Namespace,
Labels: map[string]string{"app": challenge.Name},
},
Spec: gkenetv1.ManagedCertificateSpec{
Domains: domains,
},
Status: gkenetv1.ManagedCertificateStatus{
DomainStatus: []gkenetv1.DomainStatus{},
},
}
return cert
}

func generateIngress(domainName string, challenge *kctfv1.Challenge, port *kctfv1.PortSpec) *netv1beta1.Ingress {
// Ingress object
ingress := &netv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: challenge.Name,
Namespace: challenge.Namespace,
Labels: map[string]string{"app": challenge.Name},
Annotations: map[string]string{},
},
Spec: netv1beta1.IngressSpec{
TLS: []netv1beta1.IngressTLS{{
SecretName: "tls-cert",
Expand All @@ -97,24 +127,18 @@ func generateIngress(domainName string, challenge *kctfv1.Challenge) *netv1beta1
},
}

for _, port := range challenge.Spec.Network.Ports {
// non-HTTPS is handled by generateLoadBalancerService
if port.Protocol != "HTTPS" {
continue
}
servicePort := port.Port
if servicePort == 0 {
servicePort = port.TargetPort.IntVal
}

servicePort := port.Port
if servicePort == 0 {
servicePort = port.TargetPort.IntVal
}
ingress.Spec.Backend = &netv1beta1.IngressBackend{
ServiceName: challenge.Name,
ServicePort: intstr.FromInt(int(servicePort)),
}

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

return ingress
Expand Down

0 comments on commit 45b84fc

Please sign in to comment.