HomePočetnaAboutO namaServicesUslugeISO 42001ISO 42001TeamTimIndustriesIndustrijeProjectsProjektiBlogBlogGet in TouchKontakt

Smaller Attack Surface by Design - One Gateway, Private Backends, Zero-Touch TLSManja izloženost ugrađena u arhitekturu - jedan gateway, privatni backendi, TLS bez ručnog rada

Most small and mid-size stacks still look the same: each environment gets a VM, each VM gets a public IP, SSH is open "for convenience," and TLS certificates are renewed by whoever remembered last. It works - until a scanner finds an admin panel, a forgotten staging host, or a weak password on a box that never needed to be reachable from the open internet.

We rebuilt our application infrastructure around a simpler rule: only one machine has a public IP. Everything else lives on a private network. Public traffic enters through a reverse proxy that terminates TLS and routes by hostname. Admin tools never appear on the public edge at all.

That pattern is not new. What matters is making it the default for every new product - not a one-off diagram that drifts the moment someone clicks "create server" in a console.


The shape of the network

Public users never talk to application servers directly. DNS points at a gateway. The gateway terminates HTTPS, applies policy, and proxies to private addresses. Operators reach the same private network over a VPN when they need dashboards, logs, or SSH.

Inside the private network, environments get their own subnets instead of sharing one flat range: one for development (a server per product), one for the client-facing demo and staging boxes, one for the shared monitoring stack. Hostnames follow a fixed convention per environment, so the DNS record and the proxy block for a new service are predictable rather than negotiated each time.

The practical consequences:

  • Fewer doors. Port scans of the public internet only ever see the gateway. Backend SSH, databases, monitoring UIs, log UIs, and similar tools are not listening on a public address.
  • One place for TLS. Certificates are issued and renewed at the edge, per hostname, without installing cert tooling on every app box.
  • Clearer blast radius. Compromising a public-facing listener does not automatically mean every server was internet-reachable to begin with.

Admin access is a separate path: a split-tunnel VPN that only routes the private subnet, so operator traffic to the internet does not detour through it. Each person gets their own VPN profile - a shared config passed around in chat is exactly the credential you cannot revoke for one leaver. Internal tools stay behind that gate on purpose; a login screen on the open internet is already too much exposure for monitoring and log UIs.


Why the edge matters more than the app framework

Application security still matters. But many real-world incidents start earlier: an exposed management port, an unpatched panel, brute force against /login, or SSH password auth left enabled "temporarily."

On the gateway we treat the edge as a security control plane, not just a load balancer:

  • Automatic HTTPS - certificates are obtained and renewed without a manual runbook. New hostnames get TLS when traffic starts hitting them.
  • Hostname-based routing - app.example.com maps to a private IP and port. The app never needs its own public listener.
  • Stricter rate limits on auth routes - login and token endpoints get tighter limits than ordinary traffic. That alone stops a large class of password-guessing noise before it reaches the application.
  • SSH via jump host - internal servers do not expose SSH publicly. Access goes through the gateway (or over VPN to the private address), with root login and password authentication disabled by default.

One thing we learned quickly: a rate limit nobody watches is a silent one. Graph the gateway's status codes, then alert on a sustained spike of 429 and of 401/403 on auth routes. The control becomes a signal - you find out that someone is grinding a login form now, instead of discovering it in a log months later.

None of this replaces application hardening. It removes whole classes of accidental exposure that teams create when every VM is "a little bit public."


Infrastructure as code is what keeps the pattern alive

A gateway diagram dies the first time someone provisions a server by hand. The only durable version of this design is code:

  1. Declare the topology - servers, private network, subnets, and extra disks live as infrastructure-as-code, with remote state in object storage rather than on one engineer's laptop. Adding a product means adding a resource, not inventing a new networking story.
  2. Apply the same baseline everywhere - configuration management installs users, agents, hardening drop-ins, and proxy config from a shared recipe. New boxes inherit PermitRootLogin no and PasswordAuthentication no instead of hoping someone checked the boxes.
  3. Deploy through the same path - CI builds the image, pushes it to a registry, then reaches the private target through the gateway. No "SSH in and start the containers by hand" as a production habit.

When a new product needs a home, the checklist collapses to something boring - and boring is the goal: new resource in the right subnet, baseline role, one proxy block, DNS record. Monitoring agents and logging ship with the baseline, so the box is observable on day one.


Observability that matches the layers

A single dashboard rarely answers every question. In practice we separate three concerns:

Layer Question it answers Typical approach
Host health Is the machine up? Disk full? CPU pegged? Agent-based host monitoring + chat alerts
Runtime / containers How are services behaving under load? Container and service metrics + dashboards
Application truth What did the app actually log? Centralized application logs

Gateway access logs belong in the same place as application logs. Status codes, client IPs, hostnames, and request paths turn "something feels slow" into "we are rate-limiting /login from these sources" or "that ERROR spike is an external scanner hitting a non-existent actuator path." That last one is a real example from our own index: a wall of generic "Unexpected error" entries turned out to be a harmless 404 that a global exception handler was logging at ERROR level.

Two operational details that are easy to skip and annoying to retrofit:

  • Give logs their own volume. Centralized logging plus container churn fills a root disk faster than anyone expects. A separate data disk, plus scheduled image and volume cleanup on development machines, keeps disk pressure away from the system disk.
  • Agree on the log format before shipping logs. Structured logs pay for themselves the first time you filter by service.name and log.level instead of grepping mixed container stdout - but only if every service emits them. Mixing JSON services, plain-text services, and infrastructure containers into one index means someone has to untangle it later, one service at a time.

Secrets follow the same discipline as the network: application secrets live in a central manager, split per product and per environment, and are injected at deploy time. CI authenticates to that manager as a machine identity scoped to its own project instead of borrowing an engineer's account, and deploy keys stay separate from app secrets so one compromise does not hand over both. Config that is not secret can stay in non-secret defaults - passwords and API keys should never travel over chat.


What this is really optimizing for

Cost is a nice side effect - the estate described here runs eight servers for roughly €60 a month, well under a typical large-cloud default - but the main win is operational clarity:

  • The internet only sees what you intentionally publish.
  • New environments look like the last ones.
  • TLS, deploy, monitoring, and secret injection do not depend on tribal knowledge.

If your current estate still has public SSH on every box and a shared admin URL for monitoring, you do not need a multi-year platform rewrite. Start with one rule - one public entry point - and make every new server obey it by default. The rest of the stack (proxy, VPN for operators, infrastructure as code, layered monitoring) follows naturally once that constraint is non-negotiable.

Većina malih i srednjih infrastruktura i dalje izgleda isto: svako okruženje dobije svoju virtuelnu mašinu, svaka mašina javnu IP adresu, SSH ostaje otvoren „zbog lakšeg pristupa“, a TLS sertifikate obnavlja onaj ko se posljednji sjetio. To funkcioniše - sve dok skener ne naiđe na admin panel, zaboravljeni staging host ili slabu lozinku na mašini koja nikada nije ni trebalo da bude vidljiva sa interneta.

Našu infrastrukturu za aplikacije prepravili smo oko jednog pravila: javnu IP adresu ima samo jedna mašina. Sve ostalo je u privatnoj mreži. Javni saobraćaj ulazi kroz reverse proxy koji terminira TLS i rutira po hostname-u. Admin alati se na javnoj strani ne pojavljuju uopšte.

Taj obrazac nije nov. Poenta je da postane podrazumijevan za svaki novi proizvod - a ne dijagram koji se nacrta jednom i raspadne se čim neko u konzoli klikne „create server“.


Kako izgleda mreža

Javni saobraćaj nikada ne stiže direktno do aplikacionih servera. DNS pokazuje na gateway; gateway terminira HTTPS, primjenjuje politiku i prosljeđuje zahtjev na privatne adrese. Kada im zatrebaju dashboardi, logovi ili SSH, inženjeri do iste te mreže dolaze preko VPN-a.

Unutar privatne mreže svako okruženje ima svoju podmrežu umjesto zajedničkog ravnog opsega: jedna za razvoj (po jedan server za svaki proizvod), jedna za demo i staging mašine okrenute klijentima, jedna za zajednički monitoring. Imena hostova prate fiksnu konvenciju po okruženju, pa se DNS zapis i proxy blok za novi servis znaju unaprijed umjesto da se dogovaraju svaki put iznova.

U praksi to znači:

  • Manje otvorenih ulaza. Skeniranje javnog interneta uvijek vidi samo gateway. SSH na backendu, baze, monitoring i log interfejsi i slični alati ne slušaju ni na jednoj javnoj adresi.
  • Jedno mjesto za TLS. Sertifikati se izdaju i obnavljaju na ulazu, po hostname-u, bez instaliranja cert alata na svakoj aplikacionoj mašini.
  • Jasnije granice štete. Ako neko provali kroz javni servis, to ne znači da je i ostatak flote ionako bio dostupan sa interneta.

Administrativni pristup ide potpuno drugim putem: split-tunnel VPN koji rutira samo privatnu podmrežu, pa ostatak saobraćaja inženjera ne ide zaobilazno kroz njega. Svako ima svoj VPN profil - zajednička konfiguracija koja kruži po chatu je tačno onaj kredencijal koji ne možete da opozovete kada jedan čovjek ode iz tima. Interni alati namjerno ostaju iza te kapije: login ekran na otvorenom internetu već je previše izloženosti za monitoring i logove.


Zašto je ulazna tačka često važnija od app frameworka

Sigurnost same aplikacije ostaje bitna. Ali veliki dio stvarnih incidenata počinje mnogo ranije: izložen management port, panel koji niko nije ažurirao, brute force na /login ili SSH prijava lozinkom koja je „privremeno“ ostala uključena.

Zato gateway kod nas nije samo load balancer, nego mjesto na kojem se sigurnosna politika zaista sprovodi:

  • Automatski HTTPS - sertifikati se izdaju i obnavljaju bez ijednog ručnog koraka. Novi hostname dobije TLS čim na njega krene saobraćaj.
  • Rutiranje po hostname-u - app.example.com vodi na privatnu adresu i port. Aplikacija nikada ne otvara sopstveni javni listener.
  • Stroži limiti na auth rutama - login i token endpointi dobijaju uže limite od ostatka saobraćaja. Već to odsijeca veliki dio pogađanja lozinki prije nego što uopšte stigne do aplikacije.
  • SSH preko jump hosta - interni serveri ne izlažu SSH javno. Ide se preko gateway-a ili preko VPN-a na privatnu adresu, uz isključen root login i isključenu prijavu lozinkom.

Jedno smo naučili brzo: limit koji niko ne gleda radi u tišini. Iscrtajte status kodove sa gateway-a i postavite alarm na duži skok 429 i na 401/403 na auth rutama. Tek tada kontrola postaje i signal - saznaćete da neko upravo melje login formu, umjesto da to pročitate u logu mjesecima kasnije.

Ništa od ovoga ne zamjenjuje hardening aplikacije. Ali uklanja cijelu klasu slučajne izloženosti koja nastaje kad je svaka mašina „pomalo javna“.


Infrastruktura kao kod drži ovaj pristup u životu

Dijagram ne preživi prvi server koji neko podigne ručno. Jedina verzija ovog dizajna koja traje jeste ona napisana kao kod:

  1. Topologija se deklariše - serveri, privatna mreža, podmreže i dodatni diskovi opisani su kao infrastruktura-kao-kod, sa stanjem u object storage-u umjesto na laptopu jednog inženjera. Novi proizvod znači novi resurs, a ne novu priču o mreži.
  2. Baseline je svuda isti - configuration management postavlja korisnike, agente, hardening podešavanja i proxy konfiguraciju iz istog recepta. Nova mašina nasljeđuje PermitRootLogin no i PasswordAuthentication no umjesto da se oslanjamo na to da je neko negdje štiklirao pravu opciju.
  3. Deploy ide uvijek istim putem - CI gradi image, šalje ga u registry i preko gateway-a stiže do privatnog odredišta. Bez „uloguj se pa ručno digni kontejnere“ kao produkcione navike.

Kad novom proizvodu treba mjesto, lista koraka svede se na nešto dosadno - a dosada i jeste cilj: resurs u pravoj podmreži, baseline role, jedan proxy blok, DNS zapis. Monitoring i slanje logova dolaze uz baseline, pa se mašina vidi od prvog dana.


Praćenje po slojevima

Jedan dashboard rijetko odgovara na sva pitanja, pa tri stvari držimo odvojeno:

Sloj Pitanje na koje odgovara Tipičan pristup
Zdravlje hosta Je li mašina živa? Je li disk pun? Je li CPU zakovan na 100%? Agent na hostu + alarmi u chatu
Runtime / kontejneri Kako se servisi ponašaju pod opterećenjem? Metrike kontejnera i servisa + dashboardi
Aplikacija Šta je aplikacija zaista zapisala? Centralizovani aplikacioni logovi

Access logovi sa gateway-a treba da završe na istom mjestu gdje i logovi aplikacija. Status kodovi, IP adrese klijenata, hostname-ovi i putanje pretvaraju „nešto je sporo“ u „limitiramo /login sa ovih izvora“ ili „taj skok ERROR-a je spoljni skener koji lupa na actuator putanju koje nema“. Ovo drugo nam se stvarno desilo: zid generičkih „Unexpected error“ zapisa ispostavio se kao bezopasan 404 koji je globalni exception handler pisao na ERROR nivou.

Dva operativna detalja koja je lako preskočiti, a naporno ispravljati naknadno:

  • Logovi zaslužuju svoj disk. Centralizovano logovanje i stalno dizanje i rušenje kontejnera napune root disk brže nego što iko očekuje. Zaseban disk za podatke i zakazano čišćenje image-a i volume-a na razvojnim mašinama drže sistemski disk van opasnosti.
  • Format logova se dogovara prije prvog slanja. Strukturirani logovi se isplate već prvi put kada umjesto grepanja po pomiješanom stdout-u filtrirate po service.name i log.level - ali samo ako ih šalju svi servisi. Ako u isti indeks slijeću i JSON servisi i plain-text servisi i infrastrukturni kontejneri, neko će to kasnije raspetljavati, servis po servis.

Sa tajnama radimo isto što i sa mrežom: aplikacione tajne stoje u centralnom manageru, razdvojene po proizvodu i po okruženju, i ubacuju se tek pri deploy-u. CI se tom manageru prijavljuje kao mašinski identitet vezan za svoj projekat, a ne preko naloga nekog inženjera; deploy ključevi stoje odvojeno od aplikacionih tajni, da jedan proboj ne otkrije i jedno i drugo. Ono što nije tajna može mirno ostati u običnoj konfiguraciji - lozinke i API ključevi ne putuju preko chata.


Šta zapravo dobijamo

Cijena je prijatan sporedni efekat - ovdje opisanih osam servera košta oko 60 € mjesečno, znatno ispod onoga što bi isto to koštalo kod velikog cloud provajdera - ali glavni dobitak je operativna jasnoća:

  • Internet vidi samo ono što ste namjerno objavili.
  • Svako novo okruženje liči na prethodno.
  • TLS, deploy, monitoring i ubacivanje tajni ne zavise od toga ko se čega sjeća.

Ako vaša infrastruktura i dalje ima javni SSH na svakoj mašini i zajednički admin URL za monitoring, ne treba vam višegodišnja seoba. Počnite od jednog pravila - jedna javna ulazna tačka - i neka ga svaki novi server poštuje podrazumijevano. Ostalo (proxy, VPN za inženjere, infrastruktura kao kod, praćenje po slojevima) dođe samo od sebe čim to ograničenje prestane da bude stvar pregovora.