Skip to content

About

Hardened self-hosted container platform — Keycloak OIDC SSO via Traefik forward-auth, network-segmented backend, Cloudflare Tunnel ingress, Trivy + OWASP ZAP evidence.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Secure Private Cloud Hosting Environment

A hardened self-hosted container platform — SSO-protected applications behind
a reverse proxy, network-segmented services, and no inbound ports open.

Traefik Keycloak Docker Compose Cloudflare Scanned


Enterprise Security (Cloud & IoT) — CA2 · BSc (Hons) Cloud Computing, TU Dublin. Cathal Flood · C22312211


What this is

A private cloud hosting environment built on a single Ubuntu Server 24.04 VM, designed so that every application published from it is authenticated, every backend service is unreachable from the internet, and the host exposes no inbound ports at all.

The brief was to host applications securely. The interesting part is not that it runs — it's the set of controls that make it defensible, and being able to demonstrate each one.

Architecture

                        Internet
                            │
                            ▼
                 ┌─────────────────────┐
                 │  Cloudflare Tunnel  │   outbound-only connector,
                 │  (cloudflared)      │   no inbound ports on the host
                 └──────────┬──────────┘
                            ▼
                 ┌─────────────────────┐
                 │      Traefik        │   single ingress point,
                 │   reverse proxy     │   host-based routing
                 └──────────┬──────────┘
                            │
              ┌─────────────┴──────────────┐
              ▼                            ▼
   ┌────────────────────┐       ┌────────────────────┐
   │  forward-auth      │◄──────│  protected routes  │
   │  (OIDC middleware) │       │  every request     │
   └─────────┬──────────┘       └────────────────────┘
             │ OIDC authorisation code flow
             ▼
   ┌────────────────────┐
   │      Keycloak      │   realm: securelab
   │   identity server  │   roles: app-user, app-admin
   └─────────┬──────────┘
             │
╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
   backend network — internal: true, no route to the internet
             │
      ┌──────┴──────┬───────────────┬──────────────┐
      ▼             ▼               ▼              ▼
 ┌─────────┐  ┌───────────┐  ┌───────────┐  ┌──────────────┐
 │ Postgres│  │Prometheus │  │  Grafana  │  │node-exporter │
 └─────────┘  └───────────┘  └───────────┘  └──────────────┘

Two Docker networks do the heavy lifting. frontend is a normal bridge carrying proxy-facing traffic. backend is declared internal: true, so Postgres, Prometheus and node-exporter have no route off the host — they can be reached by the services that need them and by nothing else.

Security controls

Control Implementation
No inbound exposure Cloudflare Tunnel makes an outbound-only connection; the VM's firewall needs no port-forward and the host has no public IP
Single ingress Traefik is the only route to any application; exposedByDefault: false means a container is unreachable until it explicitly opts in with a label
SSO on every protected route traefik-forward-auth runs as a Traefik middleware — requests without a valid session are bounced to Keycloak before reaching the app
Centralised identity Keycloak realm securelab with the OIDC authorisation code flow; the application holds no credentials of its own
Role-based access app-user and app-admin roles assigned in Keycloak, carried in the token
Network segmentation backend network is internal: true; the database is not reachable from the proxy tier
TLS floor minVersion: VersionTLS12 in the dynamic Traefik config
Image scanning Trivy against the Keycloak and Traefik images
Runtime testing OWASP ZAP baseline scan against the published application
Backup and restore Scripted pg_dump plus tarball of the Grafana and Prometheus volumes
Observability Prometheus scraping node-exporter and Traefik; Grafana for dashboards; Dozzle for container logs

Proving segmentation rather than claiming it

scripts/verify.sh checks that all nine containers are running, then actively tries to reach Postgres from inside the Traefik container:

docker exec traefik wget -q --timeout=2 http://postgres:5432

A failed connection is the pass condition. A control you haven't tried to break is an assumption, not a control.

Security testing

OWASP ZAP baseline

Run against the published application. Full output in security/zap/, including the HTML report.

Passed 56 rules
Warnings 11
Failures 0

The warnings are mostly transport-header issues, and they are consistent with the architecture rather than accidental: TLS terminates at the Cloudflare edge and the tunnel delivers plain HTTP to the origin, so the origin never sets Strict-Transport-Security and INSECURE_COOKIE=true is set on forward-auth for the same reason. The remainder — no CSP, no Permissions-Policy, SameSite=None on session cookies — are genuine gaps, listed below.

Trivy image scans

Results in security/trivy/.

Image Critical High
quay.io/keycloak/keycloak:24.0 2 4
traefik:v2.11 1 2

Both images are pinned to a specific minor version, which is what makes these numbers meaningful and actionable — a :latest tag would have made the scan a snapshot of nothing in particular.

Known limitations

Where this environment is not production-ready. Being able to name these is the point of a security exercise.

  • The Traefik dashboard is unauthenticated. api.insecure: true serves the dashboard without credentials. A basic-auth middleware is defined in traefik/dynamic/middlewares.yml but is not attached to the dashboard router, so it currently protects nothing.
  • Keycloak runs in development mode. start-dev with KC_HOSTNAME_STRICT: false. A production deployment needs start, a strict hostname, and its own TLS material.
  • Session cookies are not marked Secure (INSECURE_COOKIE=true), because internal routing between the tunnel and Traefik is HTTP.
  • No CSP, HSTS or Permissions-Policy on responses — ZAP flags all three.
  • No anti-CSRF tokens on the protected application (whoami is a diagnostic endpoint, not a real app, so this is inherited from the test target).
  • Docker socket is mounted read-only into Dozzle and Traefik. Read-only limits the blast radius but socket access is still effectively host-level privilege.
  • Single host, no high availability — everything shares one VM and one failure domain.

Running it

Requires Docker and Docker Compose on Ubuntu Server 24.04, and a Cloudflare Tunnel token.

git clone https://github.com/floodcathal/secure-private-cloud.git
cd secure-private-cloud
cp .env.example .env    # fill in every value
docker compose up -d
./scripts/verify.sh

Keycloak then needs a realm called securelab, a confidential client whoami-app, and the app-user / app-admin roles. Put the client secret into .env as OIDC_CLIENT_SECRET.

Service Route
Protected application app.<your-domain>
Keycloak auth.<your-domain>
Grafana metrics.<your-domain>
Dozzle logs logs.<your-domain>
Traefik dashboard traefik.<your-domain>

Backup and restore

./scripts/backup.sh              # pg_dump + Grafana/Prometheus volume tarballs
./scripts/restore.sh 20250427_143000

Repository layout

docker-compose.yml            the full stack — nine services, two networks
.env.example                  every variable the compose file reads
traefik/
  traefik.yml                 static config: entrypoints, providers, logging
  dynamic/middlewares.yml     basic-auth middleware definition
  dynamic/tls.yml             TLS floor (minVersion TLS 1.2)
prometheus.yml                scrape targets
scripts/
  verify.sh                   health + network isolation checks
  backup.sh / restore.sh      database and volume backup
security/
  trivy/                      image vulnerability scans
  zap/                        OWASP ZAP baseline scan and HTML report
docs/README.md                setup and operation notes

Notes

Credentials come entirely from environment variables — see .env.example. No .env, ACME account key, certificate or backup archive is tracked in this repository.

About

Hardened self-hosted container platform — Keycloak OIDC SSO via Traefik forward-auth, network-segmented backend, Cloudflare Tunnel ingress, Trivy + OWASP ZAP evidence.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages