Skip to project
pedromartins.tech
Operating
Secure Self-Hosted Infrastructure / Academic Publishing2026 - present

Revista BRASILCON

Published a national legal journal from self-hosted infrastructure without exposing a single inbound application port.

Role
Infrastructure architect and sole operator
Environment
Live production · Sanitized architecture
Ownership
Architecture · Deployment · Hardening · DNS · Recovery · Monitoring · Runbooks
Open live project
Revista BRASILCON project visual
brasilcon-lab5 system boundaries
Reader
Cloudflare Edge
cloudflared
OJS
0published application portsArchitecture property
3isolated production containers
1operator across day-one and day-two work
Livenational publishing platform
01 / Interactive system

Move through the system layer by layer

brasilcon-labLive production · Sanitized architecture · Read-only
Operating
System map

Boundaries and responsibilities

Public or untrustedAuthenticated boundaryInternal-only dependency
READ-ONLYLive production · Sanitized architecturetool: systemselection: reader

Reader selected.

02 / Context

The problem behind the system

BRASILCON had no digital editorial platform at all: submission, review, editorial rounds, and publication had to be built from zero, without taking on a conventional hosted-infrastructure budget.

The security challenge was unusually concrete: serve a public institution from residential infrastructure while keeping the origin and application ports out of the normal inbound attack path.

Read the full project overview

Volunteer build of the editorial management platform for the Revista de Direito do Consumidor - the journal of BRASILCON, the Brazilian Institute of Consumer Policy and Law. There was no system to migrate: the journal had no digital editorial platform, so I built one from scratch on my own server - a self-hosted Open Journal Systems (OJS) stack, now live at revistabrasilcon.com. I architected it, run it in production solo, and BRASILCON formally recognized the work in an official letter (Ofício nº 13/2026) signed by its President, the journal's Director-General, and its Secretary-General.

ZERO-OPEN-PORT INGRESS - The core security decision: the journal is published to the internet without a single inbound port. Ingress is an outbound-only Cloudflare Tunnel - the cloudflared container dials out to Cloudflare's edge, and all public traffic rides back down that authenticated connection. No port-forwards, no exposed origin IP, no direct attack surface on a residential network. TLS terminates at Cloudflare's edge, which also provides DDoS absorption and traffic filtering for free.

DEFENSE IN DEPTH BY ARCHITECTURE - The stack is three Docker containers on an isolated bridge network: a custom-built OJS 3.4 image (configuration and patches baked in at build time - the running container is reproducible, not hand-mutated), MariaDB for the editorial database, and cloudflared. The compose file publishes zero host ports: the database is reachable only by the OJS container on the internal Docker network, and OJS itself is reachable only through the tunnel. Even someone on the LAN cannot talk to the database directly.

OPERATIONS - All three containers restart automatically and survive reboots unattended. Day-2 work is mine alone: image rebuilds, DNS, tunnel health monitoring, incident documentation and runbooks. The platform digitizes the journal's full academic workflow - article submission, double-blind peer review, editorial rounds, and publication - for a national legal institute, running on hardware I own.

03 / Security

Threats, controls, and what remains

No control is presented as total risk elimination.

Network

Direct origin probing

Control
Outbound-only tunnel and zero published application ports
Residual risk
Cloudflare account security and provider availability remain dependencies.
Data

Direct database access

Control
Internal Docker network with no database host port
Residual risk
An application compromise may still reach its database dependency.
Secrets

Credentials embedded in configuration

Control
Runtime secret files instead of inline production values
Residual risk
Host-level compromise can still expose mounted secrets.
Availability

Incomplete recovery after reboot

Control
Restart policies, health checks, and documented runbooks
Residual risk
External monitoring is still required to detect partial recovery.
04 / Decisions

The trade-offs that shaped the build

Decision

Outbound-only ingress

Publish without router port forwarding.

Reason
Reduce direct origin exposure on residential infrastructure.
Trade-off
Creates a meaningful dependency on tunnel identity and provider uptime.
Revisit when
Move to redundant or managed ingress when budget and availability requirements justify it.
Decision

Internal-only database

Make the application the sole database client.

Reason
There is no operational need for LAN or public database access.
Trade-off
Administration must occur through controlled host workflows.
Revisit when
Add a dedicated management path only if operations require it.
Decision

Reproducible application image

Bake configuration and patches at build time.

Reason
Avoid hand-mutated production containers.
Trade-off
The image build and upstream base must be maintained.
Revisit when
Adopt a broader release pipeline if more operators or environments appear.
05 / Operations

The unhappy path is part of the design

01

What if the tunnel disconnects?

The public path stops while the origin remains closed to direct inbound traffic.

Signal
Tunnel health and external journal checks fail.
Response
Inspect connector state, credentials, DNS, and edge status before rebuilding.
02

What if the host reboots unattended?

Container restart policies restore the stack in dependency order.

Signal
Health checks show database readiness and application recovery.
Response
Use the runbook when any service remains unhealthy after restart.
06 / Implementation

How the decisions appear in the build

docker-compose.yml
01# Zero published host ports. The journal is reachable ONLY through the tunnel.02services:03  ojs:04    build: ./ojs                       # OJS 3.4 - patches baked in at build time05    depends_on:06      db: { condition: service_healthy }07    networks: [internal]               # no route to the outside world08    restart: unless-stopped09    # NOTE: no `ports:` block - nothing is ever bound to the host.10 11  db:12    image: mariadb:1113    environment:14      MARIADB_PASSWORD_FILE: /run/secrets/db_pass   # secret, never inline15    secrets: [db_pass]16    networks: [internal]               # reachable only by the ojs container17    volumes: [ojs_db:/var/lib/mysql]18    restart: unless-stopped19 20  cloudflared:21    image: cloudflare/cloudflared:latest22    command: tunnel --no-autoupdate run23    environment:24      TUNNEL_TOKEN_FILE: /run/secrets/cf_token25    secrets: [cf_token]26    networks: [edge, internal]         # dials OUT to Cloudflare's edge27    restart: unless-stopped28 29networks:30  edge: {}                             # cloudflared egress only31  internal: { internal: true }         # air-gapped from the public internet32 33secrets:34  db_pass:  { file: ./secrets/db_pass.txt }35  cf_token: { file: ./secrets/cf_token.txt }
Representative / sanitized excerpt

Zero published host ports - ingress is an outbound-only Cloudflare Tunnel.

OJS 3.4PHPMariaDBDocker ComposeCloudflare TunnelZero-Trust IngressDebianSelf-Hosted
07 / Evidence

Proof, source, and inspectable outcomes

08 / Results

What the project demonstrates

National journal workflow operating on infrastructure Pedro owns

Public access without a direct application-port path to the origin

Architecture, operation, and recovery owned by one accountable operator

What worked
  • The exposure model is understandable and enforceable.
  • Reproducible containers make recovery less dependent on memory.
Next iteration
  • External health monitoring
  • Encrypted off-site backups
  • Scheduled recovery exercises
Professional signal
  • Production ownership
  • Linux and container operations
  • Security architecture under real constraints