diff --git a/.env.sample b/.env.sample index c945d59..1e07241 100644 --- a/.env.sample +++ b/.env.sample @@ -14,4 +14,5 @@ LETSENCRYPT_EMAIL=admin@example.com # Uncomment next line for prod certs: LETSENCRYPT_STAGING=1 -# API_KEY=your_api_key +# Main api key for itsUP +API_KEY= diff --git a/.gitignore b/.gitignore index d7f0155..fc340fd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ .venv certs/* db.yml +proxy/docker-compose.yml proxy/nginx/*.conf proxy/traefik/*.yml upstream diff --git a/.vscode/launch.json b/.vscode/launch.json index 8fde184..46b337c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,18 @@ "PYTHONPATH": "${workspaceRoot}" } }, + { + "name": "Python: write artifacts", + "type": "debugpy", + "request": "launch", + "program": "bin/write-artifacts.py", + "console": "integratedTerminal", + "justMyCode": false, + "envFile": "${workspaceFolder}/.env", + "env": { + "PYTHONPATH": "${workspaceRoot}" + } + }, { "name": "Python Debugger: Current File", "type": "debugpy", diff --git a/.vscode/settings.json b/.vscode/settings.json index 526e7f8..39aa809 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,5 +41,8 @@ "files.exclude": { "**/.history": true }, - "python.testing.unittestEnabled": true + "python.testing.unittestEnabled": true, + "[dotenv]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + } } \ No newline at end of file diff --git a/README.md b/README.md index 13c4d9d..7bc0bae 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,25 @@ ## Fully managed docker compose infra -Single machine multi docker compose architecture with a low cpu/storage footprint and near-zero* downtime. +Single machine multi docker compose architecture with a low cpu/storage footprint and near-zero\* downtime. It runs two nginx proxies in series (proxy -> terminate) to be able to: + - terminate SSL/TLS - do SSL/TLS passthrough - target many upstream endpoints - + **Advantages:** - shared docker network: encrypted intra-node communication (over a shared network named `proxynet`) -- near-zero-downtime* +- near-zero-downtime\* -**Near-zero-downtime?* +__Near-zero-downtime?\* Well, all (stateless) nodes that get rotated do not incur downtime, yet nginx neads a reload signal. During low traffic that will allow for a graceful handling of outstanding http sessions and no downtime, but may be problematic if nginx needs to wait for a magnitude of open sessions. In that case a timeout will force the last open sessions to be terminated. This approach is a very reliable and cheap approach to achieve zero downtime. -*But what about stateful services?* +_But what about stateful services?_ It is surely possible to deploy stateful services but those MUST NOT be targeted with the `entrypoint: xxx` prop, as those services are the entrypoints which MUST be stateless, as those are rolled up with the `docker rollout` by the automation. In order to update those services you are on your own, but it's a breeze compared to local installs, as you can just docker compose commands. @@ -46,7 +47,7 @@ Install everything and start the proxy and api so that we can receive incoming c 1. `bin/install.sh`: installs all project deps. 2. `bin/start-all.sh`: starts the proxy and the api server. 3. `bin/apply.py`: applies all of `db.yml`. -4. 4. `bin/api-logs.sh`: tail the output of the api server. +4. `bin/api-logs.sh`: tail the output of the api server. ### Adding an upstream service @@ -55,9 +56,28 @@ Install everything and start the proxy and api so that we can receive incoming c ### Adding a passthrough endpoint -1. Edit `db.yml` and add your service(s), which now need `name`, `domain` and `passthrough: true`. +1. Add a project without `entrypoint:` and one service, which now need `name`, `domain` and `passthrough: true`. 2. Run `bin/apply.py` to roll out the changes. +### Adding a local (host) endpoint + +1. Add a project without `entrypoint:` and one service, which only need `name` and `domain`. +2. Run `bin/apply.py` to roll out the changes. + +### Plugins + +You can enable and configure plugins in `db.yml`. Right now we support the following: + +#### CrowdSec + +[CrowdSec](https://www.crowdsec.net) can run as a container via plugin [crowdsec-bouncer-traefik-plugin](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin). First set `enable: true`, run `bin/write-artifacts.py`, and bring up the stack (or just the `crowdsec` container: + +``` +docker compose exec crowdsec cscli bouncers add crowdsecBouncer +``` + +Put the resulting api key in the plugin configuration in `db.yml` and apply with `bin/apply.py`. + ### Api & OpenApi spec The API allows openapi compatible clients to do management on this stack (ChatGPT works wonders). @@ -71,13 +91,15 @@ Exception: Only github webhook endpoints (check for annotation `@app.hooks.regis #### Webhooks Webhooks are used for the following: + 1. to receive updates to this repo, which will result in a `git pull` and `bin/apply.py` to update any changes in the code. The provided project with `name: itsUP` is used for that, so DON'T delete it if you care about automated updates to this repo. 2. to receive incoming github webhooks (or GET requests to `/update-upstream?project=bla&service=dida`) that result in rolling up of a project or specific service only. One GitHub webhook listening to `workflow_job`s is provided, which needs: + - the hook you will register in the github project to end with `/hook?project=bla&service=dida` (`service` optional), and the `github_secret` set to `.env/API_KEY`. -I mainly use GitHub workflows and created webhooks for my individual projects, so I can just manage all webhooks in one place. +I mainly use GitHub workflows and created webhooks for my individual projects, so I can just manage all webhooks in one place. ## Dev/ops tools @@ -95,7 +117,6 @@ I don't want to switch folders/terminals all the time and want to keep history o ### Scripts - `bin/update-certs.py`: pull certs and reload the proxy if any certs were created or updated. You could run this in a crontab every week if you want to stay up to date. -- `bin/write-artifacts.py`: after updating `db.yml` yo ucan run this script to check new artifacts. +- `bin/write-artifacts.py`: after updating `db.yml` you can run this script to generate new artifacts. - `bin/validate-db.py`: after manually editing `db.yml` please run this (also ran from `bin/write-artifacts.py`) - `bin/requirements-update.sh`: You may want to update requirements once in a while ;) - diff --git a/db.yml.sample b/db.yml.sample index 4d9fcb8..6a880f7 100644 --- a/db.yml.sample +++ b/db.yml.sample @@ -1,3 +1,20 @@ +plugins: + crowdsec: + enabled: false + version: v1.2.0 +# apikey: + options: + logLevel: INFO + updateIntervalSeconds: 60 + defaultDecisionSeconds: 60 + httpTimeoutSeconds: 10 + crowdsecCapiMachineId: login + crowdsecCapiPassword: password + crowdsecCapiScenarios: + - crowdsecurity/http-path-traversal-probing + - crowdsecurity/http-xss-probing + - crowdsecurity/http-generic-bf + projects: - description: Home Assistant passthrough domain: home.example.com diff --git a/lib/data.py b/lib/data.py index 10df47f..71cf615 100644 --- a/lib/data.py +++ b/lib/data.py @@ -1,12 +1,13 @@ +from atexit import register from logging import debug, info -from typing import Callable, Dict, List +from typing import Any, Callable, Dict, List, Union, cast import yaml -from lib.models import Env, Project, Service +from lib.models import Env, Plugin, PluginRegistry, Project, Service -def get_db() -> Dict[str, List[Dict[str, str]]]: +def get_db() -> Dict[str, List[Dict[str, Any]] | Dict[str, Any]]: """Get the db""" with open("db.yml", encoding="utf-8") as f: return yaml.safe_load(f) @@ -16,18 +17,43 @@ def validate_db() -> None: """Validate db.yml contents""" debug("Validating db.yml") db = get_db() + plugins_raw = cast(Dict[str, Any], db["plugins"]) + for name, plugin in plugins_raw.items(): + p = {"name": name, **plugin} + Plugin.model_validate(p) for project in db["projects"]: Project.model_validate(project) +def get_plugin_registry() -> PluginRegistry: + """Get plugin registry.""" + debug("Getting plugin registry") + db = get_db() + plugins_raw = cast(Dict[str, Any], db["plugins"]) + return PluginRegistry(**plugins_raw) + + +def get_plugins(filter: Callable[[Plugin], bool] = None) -> List[Plugin]: + """Get all plugins. Optionally filter the results.""" + debug("Getting plugins" + (f" with filter {filter}" if filter else "")) + registry = get_plugin_registry() + plugins = [] + for name, p in registry: + plugin = Plugin(name=name, **p) + if not filter or filter(plugin): + plugins.append(plugin) + return plugins + + def get_projects(filter: Callable[[Project, Service], bool] = None) -> List[Project]: """Get all projects. Optionally filter the results.""" debug("Getting projects" + (f" with filter {filter}" if filter else "")) db = get_db() + projects_raw = cast(List[Dict[str, Any]], db["projects"]) ret = [] - for p_json in db["projects"]: + for project in projects_raw: services = [] - p = Project(**p_json) + p = Project(**project) for s in p.services.copy(): if not filter or filter(p, s): services.append(s) @@ -83,7 +109,6 @@ def get_service(project: str | Project, service: str, throw: bool = True) -> Ser """Get a project's service by name""" debug(f"Getting service {service} in project {project.name if isinstance(project, Project) else project}") p = get_project(project, throw) if isinstance(project, str) else project - assert p is not None for item in p.services: if item.name == service: return item @@ -98,7 +123,6 @@ def get_env(project: str | Project, service: str) -> Dict[str, str]: """Get a project's env by name""" debug(f"Getting env for service {service} in project {project.name if isinstance(project, Project) else project}") service = get_service(project, service) - assert service is not None return service.env @@ -106,9 +130,7 @@ def upsert_env(project: str | Project, service: str, env: Env) -> None: """Upsert the env of a service""" p = get_project(project) if isinstance(project, str) else project debug(f"Upserting env for service {service} in project {p.name}: {env.model_dump_json()}") - assert p is not None s = get_service(p, service) - assert s is not None s.env = Env(**(s.env.model_dump() | env.model_dump())) upsert_service(project, s) @@ -117,9 +139,7 @@ def upsert_service(project: str | Project, service: Service) -> None: """Upsert a service""" p = get_project(project) if isinstance(project, str) else project debug(f"Upserting service {service.name} in project {p.name}: {service}") - assert p is not None for i, s in enumerate(p.services): - assert s is not None if s.name == service.name: p.services[i] = service break diff --git a/lib/data_test.py b/lib/data_test.py index 5f53f4d..a9fe372 100644 --- a/lib/data_test.py +++ b/lib/data_test.py @@ -35,7 +35,7 @@ def test_write_projects(self, mock_open: Mock, mock_yaml: Mock) -> None: mock_open.assert_called_once_with("db.yml", "w", encoding="utf-8") # Assert that the mock functions were called correctly - mock_yaml.dump.assert_called_once_with(test_db, mock_open()) + mock_yaml.dump.assert_called_once_with({"projects": test_db["projects"]}, mock_open()) # Get projects with filter @mock.patch( diff --git a/lib/models.py b/lib/models.py index 2c99070..3629e40 100644 --- a/lib/models.py +++ b/lib/models.py @@ -8,6 +8,27 @@ class Env(BaseModel): model_config = ConfigDict(extra="allow") +class Plugin(BaseModel): + """Plugin model""" + + enabled: bool = False + """Wether or not the plugin is enabled""" + apikey: str = None + """The API key to use for the plugin, if the plugin requires one""" + name: str = None + """The name of the plugin""" + description: str = None + """A description of the plugin""" + options: Dict[str, Any] = {} + """A dictionary of options to pass to the plugin""" + + +class PluginRegistry(BaseModel): + """Plugin registry""" + + crowdsec: Plugin + + class Service(BaseModel): """Service model""" diff --git a/lib/proxy.py b/lib/proxy.py index b494fd6..fcc2a6e 100644 --- a/lib/proxy.py +++ b/lib/proxy.py @@ -5,7 +5,7 @@ from dotenv import load_dotenv from jinja2 import Template -from lib.data import get_project, get_projects +from lib.data import get_plugin_registry, get_project, get_projects from lib.utils import run_command load_dotenv() @@ -82,9 +82,12 @@ def write_routers() -> None: t = f.read() tpl_routers_web = Template(t) domain = os.environ.get("TRAEFIK_DOMAIN") - traefik_admin = os.environ.get("TRAEFIK_ADMIN") routers_web = tpl_routers_web.render( - projects=projects, traefik_rule=f"Host(`{domain}`)", traefik_admin=traefik_admin + projects=projects, + traefik_rule=f"Host(`{domain}`)", + traefik_admin=os.environ.get("TRAEFIK_ADMIN"), + plugin_registry=get_plugin_registry(), + trusted_ips_cidrs=os.environ.get("TRUSTED_IPS_CIDRS").split(","), ) with open("proxy/traefik/routers-web.yml", "w", encoding="utf-8") as f: f.write(routers_web) @@ -94,28 +97,49 @@ def write_routers() -> None: routers_tcp = tpl_routers_tcp.render(projects=projects, traefik_rule=f"HostSNI(`{domain}`)") with open("proxy/traefik/routers-tcp.yml", "w", encoding="utf-8") as f: f.write(routers_tcp) + + +def write_config() -> None: with open("proxy/tpl/config-tcp.yml.j2", encoding="utf-8") as f: t = f.read() tpl_config_tcp = Template(t) - trusted_ips_cidr = os.environ.get("TRUSTED_IPS_CIDRS").split(",") - config_tcp = tpl_config_tcp.render(trusted_ips_cidr=trusted_ips_cidr) + trusted_ips_cidrs = os.environ.get("TRUSTED_IPS_CIDRS").split(",") + config_tcp = tpl_config_tcp.render(trusted_ips_cidrs=trusted_ips_cidrs) with open("proxy/traefik/config-tcp.yml", "w", encoding="utf-8") as f: f.write(config_tcp) with open("proxy/tpl/config-web.yml.j2", encoding="utf-8") as f: t = f.read() tpl_config_web = Template(t) - le_email = os.environ.get("LETSENCRYPT_EMAIL") - le_staging = os.environ.get("LETSENCRYPT_STAGING") - config_web = tpl_config_web.render(trusted_ips_cidr=trusted_ips_cidr, le_email=le_email, le_staging=le_staging) + plugin_registry = get_plugin_registry() + has_plugins = any(plugin.enabled for _, plugin in plugin_registry) + config_web = tpl_config_web.render( + trusted_ips_cidrs=trusted_ips_cidrs, + le_email=os.environ.get("LETSENCRYPT_EMAIL"), + le_staging=bool(os.environ.get("LETSENCRYPT_STAGING")), + plugin_registry=plugin_registry, + has_plugins=has_plugins, + ) with open("proxy/traefik/config-web.yml", "w", encoding="utf-8") as f: f.write(config_web) +def write_compose() -> None: + plugin_registry = get_plugin_registry() + with open("proxy/tpl/docker-compose.yml.j2", encoding="utf-8") as f: + t = f.read() + tpl_compose = Template(t) + compose = tpl_compose.render(plugin_registry=plugin_registry) + with open("proxy/docker-compose.yml", "w", encoding="utf-8") as f: + f.write(compose) + + def write_proxies() -> None: write_maps() write_proxy() write_terminate() write_routers() + write_config() + write_compose() def update_proxy( diff --git a/lib/proxy_test.py b/lib/proxy_test.py index 4dc3cbe..3d430af 100644 --- a/lib/proxy_test.py +++ b/lib/proxy_test.py @@ -164,10 +164,14 @@ def test_write_terminate(self, _: Mock, mock_get_domains: Mock, mock_open: Mock) @mock.patch("lib.proxy.write_maps") @mock.patch("lib.proxy.write_proxy") @mock.patch("lib.proxy.write_terminate") + @mock.patch("lib.proxy.write_compose") + @mock.patch("lib.proxy.write_config") @mock.patch("lib.proxy.write_routers") def test_write_proxies( self, mock_write_routers: Mock, + mock_write_config: Mock, + mock_write_compose: Mock, mock_write_terminate: Mock, mock_write_proxy: Mock, mock_write_maps: Mock, @@ -180,6 +184,9 @@ def test_write_proxies( mock_write_maps.assert_called_once() mock_write_proxy.assert_called_once() mock_write_terminate.assert_called_once() + mock_write_compose.assert_called_once() + mock_write_config.assert_called_once() + mock_write_routers.assert_called_once() @mock.patch("lib.proxy.run_command") def test_reload_proxy(self, mock_run_command: Mock) -> None: diff --git a/proxy/docker-compose.yml b/proxy/docker-compose.yml index 38802c0..31d420a 100755 --- a/proxy/docker-compose.yml +++ b/proxy/docker-compose.yml @@ -65,8 +65,6 @@ services: traefik-web: image: traefik:v2.11 container_name: traefik-web - depends_on: - - dockerproxy env_file: - ../.env networks: @@ -76,6 +74,30 @@ services: - '8080' - '8443' volumes: + - logs:/var/log/traefik - ./traefik/config-web.yml:/etc/traefik/traefik.yml:ro - ./traefik/routers-web.yml:/etc/traefik/routers-web.yml:ro - ./traefik/acme:/etc/acme + depends_on: + - dockerproxy + - traefik-tcp + - crowdsec + crowdsec: + image: crowdsecurity/crowdsec:v1.6.0 + container_name: crowdsec + restart: unless-stopped + environment: + GID: ${GID-1000} + COLLECTIONS: crowdsecurity/linux crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/sshd crowdsecurity/whitelist-good-actors + CUSTOM_HOSTNAME: crowdsec + BOUNCER_KEY_TRAEFIK: L9yZ1y2XagDm9915mRg2fg== + volumes: + - ./traefik/crowdsec/acquis.yml:/etc/crowdsec/acquis.yaml:ro + - logs:/var/log/traefik:ro + - crowdsec-db:/var/lib/crowdsec/data/ + labels: + - "traefik.enable=false" + +volumes: + logs: + crowdsec-db: \ No newline at end of file diff --git a/proxy/tpl/config-tcp.yml.j2 b/proxy/tpl/config-tcp.yml.j2 index b97fd6b..3efa216 100644 --- a/proxy/tpl/config-tcp.yml.j2 +++ b/proxy/tpl/config-tcp.yml.j2 @@ -5,13 +5,13 @@ entryPoints: forwardedHeaders: trustedIPs: - 127.0.0.0/32 -{%- for ip in trusted_ips_cidr %} +{%- for ip in trusted_ips_cidrs %} - {{ ip }} {%- endfor %} proxyProtocol: trustedIPs: - 127.0.0.0/32 -{%- for ip in trusted_ips_cidr %} +{%- for ip in trusted_ips_cidrs %} - {{ ip }} {%- endfor %} @@ -20,13 +20,13 @@ entryPoints: forwardedHeaders: trustedIPs: - 127.0.0.1/32 -{%- for ip in trusted_ips_cidr %} +{%- for ip in trusted_ips_cidrs %} - {{ ip }} {%- endfor %} proxyProtocol: trustedIPs: - 127.0.0.0/32 -{%- for ip in trusted_ips_cidr %} +{%- for ip in trusted_ips_cidrs %} - {{ ip }} {%- endfor %} diff --git a/proxy/tpl/config-web.yml.j2 b/proxy/tpl/config-web.yml.j2 index 5d7ba61..ead540c 100644 --- a/proxy/tpl/config-web.yml.j2 +++ b/proxy/tpl/config-web.yml.j2 @@ -1,10 +1,15 @@ -accessLog: {} +accessLog: + filePath: /var/log/traefik/access.log # format: json # fields: # defaultMode: keep # headers: # defaultMode: keep +log: + filePath: /var/log/traefik/error.log + level: DEBUG + entryPoints: web: address: ':8080' @@ -56,10 +61,15 @@ certificatesResolvers: experimental: http3: true +{%- if has_plugins %} + plugins: + {%- if plugin_registry.crowdsec.enabled %} + {%- set p = plugin_registry.crowdsec %} + bouncer: + modulename: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin + version: {{ p.version }} + {%- endif %} +{%- endif %} global: sendAnonymousUsage: true - -log: - level: DEBUG - diff --git a/proxy/tpl/docker-compose.yml.j2 b/proxy/tpl/docker-compose.yml.j2 new file mode 100755 index 0000000..94145df --- /dev/null +++ b/proxy/tpl/docker-compose.yml.j2 @@ -0,0 +1,107 @@ +--- +version: '3.8' + +networks: + proxynet: + name: proxynet + driver: bridge + driver_opts: + encrypted: 'true' + +services: + # watchtower: + # container_name: watchtower + # image: containrrr/watchtower:1.7.1 + # networks: + # - traefik + # restart: always + + dockerproxy: + image: wollomatic/socket-proxy:1 # see https://github.com/wollomatic/simple-traefik for reference + container_name: dockerproxy + # this image replaced https://github.com/Tecnativa/docker-socket-proxy as the socket proxy + # for an example with the tecnativa proxy refer tags before 2.10 + # depends_on: + # - watchtower + command: + # with this configuration socket-proxy acts similar to the tecnativa proxy. For additional hardening + # please refer to the documentation of the wollomatic/socket-proxy image + - '-loglevel=info' # set to debug for far more logging + - '-allowfrom=traefik-web' + - '-listenip=0.0.0.0' + - '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)' # this regexp allows readonly access only for requests that traefik needs + - '-shutdowngracetime=5' + - '-watchdoginterval=600' + - '-stoponwatchdog' + read_only: true + mem_limit: 64M + cap_drop: + - ALL + security_opt: + - no-new-privileges + user: '0:1' # replace with the uid:gid of the host owner of the docker socket + networks: + - default + expose: + - '2375' + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + traefik-tcp: + image: traefik:v2.11 + container_name: traefik-tcp + env_file: + - ../.env + networks: + - default + ports: + - 8080:8080 + - 8443:8443 + volumes: + - ./traefik/config-tcp.yml:/etc/traefik/traefik.yml:ro + - ./traefik/routers-tcp.yml:/etc/traefik/routers-tcp.yml:ro + + traefik-web: + image: traefik:v2.11 + container_name: traefik-web + env_file: + - ../.env + networks: + - default + - proxynet + expose: + - '8080' + - '8443' + volumes: + - logs:/var/log/traefik + - ./traefik/config-web.yml:/etc/traefik/traefik.yml:ro + - ./traefik/routers-web.yml:/etc/traefik/routers-web.yml:ro + - ./traefik/acme:/etc/acme + depends_on: + - dockerproxy + - traefik-tcp +{%- if plugin_registry.crowdsec.enabled %} + - crowdsec + crowdsec: + image: crowdsecurity/crowdsec:v1.6.0 + container_name: crowdsec + restart: unless-stopped + environment: + GID: ${GID-1000} + COLLECTIONS: crowdsecurity/linux crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/sshd crowdsecurity/whitelist-good-actors + CUSTOM_HOSTNAME: crowdsec + {%- if plugin_registry.crowdsec.apikey %} + BOUNCER_KEY_TRAEFIK: {{ plugin_registry.crowdsec.apikey }} + {%- endif %} + volumes: + - ./traefik/crowdsec/acquis.yml:/etc/crowdsec/acquis.yaml:ro + - logs:/var/log/traefik:ro + - crowdsec-db:/var/lib/crowdsec/data/ + labels: + - "traefik.enable=false" +{%- endif %} + +volumes: + logs: + crowdsec-db: diff --git a/proxy/tpl/routers-web.yml.j2 b/proxy/tpl/routers-web.yml.j2 index 2026b56..916d5e2 100644 --- a/proxy/tpl/routers-web.yml.j2 +++ b/proxy/tpl/routers-web.yml.j2 @@ -6,12 +6,15 @@ http: - web middlewares: - redirect + - crowdsec rule: Host(`*`) traefik-secure: service: api@internal entryPoints: - web-secure middlewares: + - default-headers + - rate-limit - traefik-auth rule: {{ traefik_rule }} tls: @@ -31,6 +34,12 @@ http: tls: certResolver: letsencrypt {%- endif %} + middlewares: + - default-headers + - rate-limit + {%- if plugin_registry.crowdsec.enabled %} + - crowdsec + {%- endif %} {%- endif %} {%- endfor %} services: @@ -59,3 +68,38 @@ http: traefik-auth: basicauth: users: {{ traefik_admin }} + default-headers: + headers: + frameDeny: true + sslRedirect: true + browserXssFilter: true + contentTypeNosniff: true + # HSTS - uncomment for HSTS + #forceSTSHeader: true + #stsIncludeSubdomains: true + #stsPreload: true + rate-limit: + rateLimit: + average: 100 + burst: 50 +{%- if plugin_registry.crowdsec.enabled %} + crowdsec: + plugin: + bouncer: +{%- for k, v in plugin_registry.crowdsec.options.items() %} + {{ k }}: {{ v }} +{%- endfor %} + enabled: true + forwardedHeadersTrustedIPs: + - 172.0.0.0/8 + - 192.168.0.0/16 + clientTrustedIPs: {{ trusted_ips_cidrs }} + crowdsecMode: live + crowdsecAppsecEnabled: true + crowdsecAppsecFailureBlock: true + crowdsecAppsecHost: crowdsec:7422 + crowdsecLapiKey: {{ plugin_registry.crowdsec.apikey }} + crowdsecLapiHost: crowdsec:8080 + crowdsecLapiScheme: http + crowdsecLapiTLSInsecureVerify: false +{%- endif %} \ No newline at end of file diff --git a/proxy/traefik/acme/acme.json b/proxy/traefik/acme/acme.json index befaaaa..ef81140 100644 --- a/proxy/traefik/acme/acme.json +++ b/proxy/traefik/acme/acme.json @@ -1,14 +1,17 @@ { "letsencrypt": { "Account": { - "Email": "", + "Email": "admin@instrukt.ai", "Registration": { "body": { - "status": "valid" + "status": "valid", + "contact": [ + "mailto:admin@instrukt.ai" + ] }, - "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/137175483" + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1583328607" }, - "PrivateKey": "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", + "PrivateKey": "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", "KeyType": "4096" }, "Certificates": [ diff --git a/proxy/traefik/crowdsec/acquis.yml b/proxy/traefik/crowdsec/acquis.yml new file mode 100644 index 0000000..406d012 --- /dev/null +++ b/proxy/traefik/crowdsec/acquis.yml @@ -0,0 +1,12 @@ +--- +filenames: + - /var/log/traefik/access.log +labels: + type: traefik +# --- +# listen_addr: 0.0.0.0:7422 +# appsec_config: crowdsecurity/virtual-patching +# name: myAppSecComponent +# source: appsec +# labels: +# type: appsec