{"id":13509264,"url":"https://github.com/adamyi/CTFProxy","last_synced_at":"2025-03-30T13:31:54.977Z","repository":{"id":42757218,"uuid":"278833416","full_name":"adamyi/CTFProxy","owner":"adamyi","description":"Your ultimate infrastructure to run a CTF, with a BeyondCorp-like zero-trust network and simple infrastructure-as-code configuration.","archived":false,"fork":false,"pushed_at":"2022-12-22T17:27:51.000Z","size":610,"stargazers_count":65,"open_issues_count":11,"forks_count":12,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-08-02T02:13:31.096Z","etag":null,"topics":["capture-the-flag","ctf","ctf-events","ctf-framework","ctf-platform","ctf-scoreboard","ctf-tools","ctfd","education","proxy","security","zero-trust"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adamyi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-11T09:51:33.000Z","updated_at":"2024-05-04T22:09:37.000Z","dependencies_parsed_at":"2023-01-30T15:01:01.736Z","dependency_job_id":null,"html_url":"https://github.com/adamyi/CTFProxy","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamyi%2FCTFProxy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamyi%2FCTFProxy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamyi%2FCTFProxy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamyi%2FCTFProxy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adamyi","download_url":"https://codeload.github.com/adamyi/CTFProxy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222552791,"owners_count":17002158,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["capture-the-flag","ctf","ctf-events","ctf-framework","ctf-platform","ctf-scoreboard","ctf-tools","ctfd","education","proxy","security","zero-trust"],"created_at":"2024-08-01T02:01:05.419Z","updated_at":"2024-11-01T09:31:13.992Z","avatar_url":"https://github.com/adamyi.png","language":"Go","funding_links":[],"categories":["Go","Go (134)","security"],"sub_categories":[],"readme":"# CTFProxy\n\nYour ultimate CTF infrastructure, with a BeyondCorp-like Zero-Trust Network and simple infrastructure-as-code configuration.\n\nThis was used for [UNSW's Web Application Security course (COMP6443 and COMP6843)](https://webcms3.cse.unsw.edu.au/COMP6443/20T2/), for which we are running 50+ challenges with 100+ containers. Only a single command is used to build all the containers (written in Golang, Python, PHP, Jsonnet, etc.). We also have CI server setup to perform auto linting, compiling, pushing, and deploying. We have a test environment running docker-compose and a prod environment running Kubernetes. All configurations live in a single monorepo and do not require manual (undeterministic) work.\n\nPlayers are SSO-authenticated with either mTLS certificate or username/password. This authentication info is then passed as JWT to internal services. Inter-service communication is also authenticated via IP address and signed with JWT tokens. All challenges and infra services have a single configuration file called `challenge.libsonnet` that defines its domain names, service images, replicas, health check, flags, and access control policies. Access control policies are defined in [Starlark](https://github.com/bazelbuild/starlark), a dialect of Python to allow really fine-grained access control.\n\nThis is a fork of our original [Geegle3](https://github.com/adamyi/Geegle3)\n\n## Infra Microservices\n\n- ctfd: CTFd scoreboard (it doesn't require any manual setup for challenges. It's calling the flaganizer microservice)\n- ctfproxy: TLS (HTTPS) termination, SSO, Access Control, Domain Routing, Centralized Logging\n- dns: this is used only for `docker` deployment (not used with `k8s`) to help with inter-service communication\n- elk: elasticsearch stack for ctfproxy logging\n- flaganizer: flag signing and verification service (supports unique flag per player)\n- gaia: internal user authentication and groups microservice\n- isodb: isolated database as a service\n- requestz: a simple network debugging tool\n- whoami: shows you who you are\n- xssbot: headless chrome as a service (puppeteer queue), integrated with ctfproxy auth\n\nWe are, by design, using an external MySQL database out of the cluster so that we won't risk losing the data in a container.\n\nIt's a TODO to add PostgreSQL support to isodb, so we can use the same isodb RPC interface to connect to different backends\nfor various SQL injection challenges.\n\n## Installation\n\n### Configuration\n\nFirst fork this repo!\n\nYou need to configure your domain name and container registry in [config.bzl](config.bzl).\n\nYou also need to add a few commandline arguments to infra services as configuration. You can grep for `FIXME` to find them\n\n```\n$ grep -r FIXME infra\ninfra/ctfproxy/BUILD.bazel:        # FIXME: configure ssl_cert, ssl_key, mtls_ca here\ninfra/ctfproxy/BUILD.bazel:        # FIXME: configure cert_gcs_bucket gcp_service_account gcp_project here\ninfra/flaganizer/BUILD.bazel:        # FIXME: configure flag_key here\ninfra/gaia/BUILD.bazel:        # FIXME: configure dbaddr here\ninfra/isodb/BUILD.bazel:        # FIXME: configure dbaddr here\ninfra/ctfd/BUILD.bazel:        # FIXME: configure DATABASE_URL and SECRET_KEY here\n```\n\n### Challenge Creation\n\nTODO(adamyi): better documentation\n\nEvery challenge lives under its own directory under `challenges/` with a single configuration file `challenges.libsonnet`\n\nAn example configuration looks like this:\n\n```\n{\n  services: [\n    {\n      name: 'welcome',\n      replicas: 3,\n      category: 'week0',\n      clustertype: 'master',\n      access: 'grantAccess()',\n    },\n  ],\n  flags: [\n    {\n      Id: 'welcome_flag_dynamic',  // globally unique flag id among entire monorepo\n      DisplayName: 'Welcome!',  // a display name of the flag in submission history visible to players on CTFd\n      Category: 'misc',  // a challenge category visible to players on CTFd\n      Points: 1,  // points associated with the flag\n      Type: 'dynamic',  // unique flag per user\n      Flag: 'WELCOME_TO_CTFPROXY',  // the flag will look like FLAG{WELCOME_TO_CTFPROXY.XXX.YYY}\n    },\n    {\n      Id: 'welcome_flag_fixed',  // globally unique flag id among entire monorepo\n      DisplayName: 'Welcome!',  // a display name of the flag in submission history visible to players on CTFd\n      Category: 'misc',  // a challenge category visible to players on CTFd\n      Points: 1,  // points associated with the flag\n      Type: 'fixed',  // fixed static flag\n      Flag: 'WELCOME2CTFPROXY',  // the flag will look like FLAG{WELCOME2CTFPROXY}\n    },\n  ],\n}\n```\n\nThis is all you need for your challenge - no need to configure CTFd manually. The challenge will be added lazily on CTFd.\n\nThe structure of your challenges are going to look very similar to the infra services under [infra](infra) directory. In `BUILD.bazel` file, you need to define a `:image` target using [rules_docker](https://github.com/bazelbuild/rules_docker).\n\nWhile you can still use Dockerfile (there's a `dockerfile_image` rule in bazel rules_docker), try using `container_image` instead. Unlike Dockerfile, this ensures that the building process is actually deterministic/reproduceable. Use all existing infra/challenges BUILD files as reference, also learn more about it here: https://github.com/bazelbuild/rules_docker/tree/master/testing/examples.\n\nAfter creating your challenge, run `bazel run //tools/ctflark` to update `challenges_list` and `container_bundle` (this is automatic, you just need to run it)\n\nUser authentication info is passed to challenges in `X-CTFProxy-JWT` header. Response headers starting with `X-CTFProxy-I-` are only shown to staff, not to players. They also appear in ELK logs. Use this for better logging/debugging.\n\nPlease have a `/healthz` endpoint (this path is also configurable) on your challenge returning a 200. This is for load balancing and health checking purpose. Your instance will be killed and respawned if health check fails in k8s.\n\n### Domain Routing\n\nDomain routing is handled automatically by CTFProxy. All you need is set up the `challenge.libsonnet` file. Specifically, your domain will be `{service_name}.{ctf_domain}`. Multi-level subdomains are also supported. E.g., if my service is named `dev-dot-blog`, and my ctf_domain is configured as `adamyi.com`. The service will live on `https://dev.blog.adamyi.com`.\n\nIt's recommended that you set up a wildcard record to CTFProxy so you don't need to worry about the rest (you get a nice error page if the domain doesn't exist). If you prefer to only have records for existing domains, we have our infra to set up AWS Route53 configuration. Just run `bazel build //infra/jsonnet:route53`. Please send a pull request if you added integration with other cloud providers.\n\nTLS (HTTPS) is terminated by CTFProxy. You don't need to set up anything manually. It automatically provisions your certificate using ACME protocol with Let's Encrypt. If you wish, you can also supply your own certificates by providing filepath to ctfproxy's commandline argument. Edit `infra/ctfproxy/BUILD.bazel` for this. Auto certs are propagated with GCS (Google Cloud Storage) among replicas.\n\n### Access Control\n\nAs mentioned above, you are using Starlark for access control policy. A more complex example would be:\n\n```\n      access: |||\n        def checkAccess():\n          if method != \"GET\" and path != \"/wp-login.php\":\n            denyAccess()\n          if path.startswith(\"/wp-admin/plugin\") or path.startswith(\"/wp-admin/option\") or path.startswith(\"/wp-admin/theme\"):\n            denyAccess()\n          if path in [\"/wp-admin/customize.php\", \"/wp-admin/widgets.php\", \"/wp-admin/nav-menus.php\", \"/wp-admin/site-health.php\", \"/wp-admin/tools.php\", \"/wp-admin/import.php\", \"/wp-admin/export.php\", \"/wp-admin/erase-personal-data.php\", \"/wp-admin/export-personal-data.php\", \"/wp-admin/update-core.php\"]:\n            denyAccess()\n          if path == \"/wp-admin/users.php\" and len(query) \u003e 0:\n            denyAccess()\n          # only staff has access for now, because it's not yet released\n          if (\"staff@groups.\" + corpDomain) in groups:\n            grantAccess()\n        checkAccess()\n      |||,\n```\n\nIn this example, we made a shared Wordpress instance's admin portal safe in a CTF environment.\n\nSpecifically, you can call the following functions:\n\n- denyAccess() - deny access\n- grantAccess() - grant access provided that the user has logged in\n- openAccess() - grant access no matter if the user is logged in\n\nYou also have access to the following variables:\n\n- host: request hostname\n- method: request method\n- path: request path\n- rawpath: request path (url-encoded raw path)\n- query: raw query string\n- ip: remote IP address\n- user: SSO username\n- corpDomain: your configured CTF domain\n- groups: SSO groups that user is in\n- timestamp: current unix milli epoch timestamp\n\n### Building\n\nPlease build using Linux AMD64. Cuz it's hard to set up cross-compiling for C programs on mac, ceebs.\n\nBuild only:\n\n```\nbazel build //:all_containers\n```\n\nBuild and tag locally (so that you can use docker-compose to boot them up):\n\n```\nbazel run //:all_containers\n```\n\n### Deploying (kubernetes, alpha)\n\nWe support both kubernetes and docker-compose!\n\nWhile the recommended practice is to use k8s, docker-compose has not been deprecated yet. This means that we shouldn't introduce any breaking changes that rely solely on k8s api. Everything should be able to run on just docker.\n\nThe approach we adopted for COMP6443 is that we use docker-compose for local dev \u0026 testing, because it's simple to set up. Use k8s cluster for prod high-availability deployment.\n\n```\nbazel build //infra/jsonnet:k8s\nkubectl apply -f dist/bin/infra/k8s.yaml\n```\n\n**Rationale for Switching from Docker-Compose to K8S**\n\nOur manual approach for managing IP addresses works great, but we can only add service to the end of list so it gets a new ip range. Otherwise, we're changing ip addresses for existing docker networks and this requires a full reboot with downtime.\n\nk8s gives us better service discovery and can maintain zero-downtime for adding and updating services. k8s also gives us rolling updates, replicas, scaling, etc. Also, for isolated challenges, we no longer need one server per student in k8s infra (this is not yet supported with k8s deployment and is a TODO).\n\nThe downside of k8s is that it's such a pain to set up a working cluster... But after it's set up, everything just works (as long as you know what you're doing).\n\n### Deploying (docker-compose, legacy stable)\n\n#### Main Server (Shared Server)\n\n```\nbazel build //infra/jsonnet:cluster-master-docker-compose\ndocker-compose -f dist/bin/infra/jsonnet/cluster-master-docker-compose.json up -d\n```\n\n#### Team Server (Separate Isolated Server)\n\n```\nbazel build //infra/jsonnet:cluster-team-docker-compose\ndocker-compose -f dist/bin/infra/jsonnet/cluster-team-docker-compose.json up -d\n```\n\n#### Test Server (All-in-one Server)\n\n```\nbazel build //infra/jsonnet:all-docker-compose\ndocker-compose -f dist/bin/infra/jsonnet/all-docker-compose.json up -d\n```\n\n## Coding Style\n\n- Run `bazel run //tools/ctflark` to update `challenges_list` and `container_bundle` for challenges.\n- For golang, we use the go standard style. Please use `bazel run @go_sdk//:bin/gofmt -- -s -d .` to format your code. We use `nogo` to lint your code at compile-time. Use `bazel run //:gazelle` to generate BUILD files.\n- For python, we use yapf style, which is like PEP8, but uses 2-space indentation instead of 4. Please use `yapf --in-place --recursive --parallel .` to format your code. Use `pylint` as a linter (not yet enabled due to failed linting). Please ensure code is python2-compatible.\n- For jsonnet, please use `tools/format_jsonnet.sh` to format your code.\n- For bazel, use `bazel run //:buildifier` to format BUILD files.\n\nThere's a nice `tools/format.sh` to run all commands above.\n\nYou can run `tools/PRESUBMIT.sh` to check if your code meets all style requirements and compiles correctly.\n\n## Contributing\n\nYou're welcome to contribute! Just send a pull request.\n\n## Author\n\n[Adam Yi](https://github.com/adamyi)\n\n## LICENSE\n\nOpen-sourced with love, under [Apache 2.0 License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamyi%2FCTFProxy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadamyi%2FCTFProxy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamyi%2FCTFProxy/lists"}