Here’s a situation I dealt with recently: A healthmed software company with several medical clinics was coming online and desperately needed a seamless way to connect to their customers' medical office computers and databases. Everyone on the call was clear about the goal: secure, controlled access to the critical systems at these clinics without exposing them to the public internet or relying on VPN setups.
As a customer engineer, I’m on that call to paint the picture of how they can get this kind of site-to-site connectivity network up and running quickly and securely. Here’s the picture I came up with:
Now, let’s go into a deep dive into why and how this works so well not just for tech-rich clinics, but any situation where you need to secure access to a customer's network.
Every site-to-site connectivity architecture has a few shared problems:
In this healthmed company’s situation, they needed to access three key services on each clinic’s network:
By deploying a single ngrok agent per clinic, we can achieve secure, reliable, and dynamically controlled access to all systems within a customer’s network without exposing any internal machines to the public internet.
For this particular set up, I recomment a custom agent and custom agent connect URLs so that customers associate the point of connection with your brand. Connect with our team to get these set up.
Traditionally, you might assume that every device inside the clinic needs its own ngrok agent, but this is unnecessary. A single ngrok agent is installed on a network-accessible server inside the clinic, and it:
This setup minimizes security risks, simplifies deployment, and ensures continuous uptime for mission-critical services.
An internal endpoint enables a service inside the clinic network to be reachable within ngrok, without being publicly exposed.
Here’s an example**:** The clinic’s REST API runs on a local server (192.168.1.100:8080
).
Instead of exposing it publicly, you can create an internal endpoint:
endpoints:
- name: api
url: https://api.internal
upstream:
url: http://192.168.1.100:8080
Now, this API is only accessible inside ngrok’s private network.
A cloud endpoint is a permanent, externally accessible entry point into the factory network.
Example: The clinic’s REST API is accessible via https://clinic.example.com/api
, but instead of exposing the API directly, a cloud endpoint forwards traffic to its internal endpoint:
on_http_request:
- expressions:
- req.url.path.startsWith("/api")
actions:
- type: forward-internal
config:
url: "https://api.internal
After installing the ngrok agent, define all required internal endpoints inside your ngrok configuration file.
version: 3
agent:
authtoken: <YOUR_NGROK_AUTHTOKEN>
endpoints:
- name: rest-api
url: https://rest-api.internal
upstream:
url: http://192.168.1.100:8080 # API inside clinic network
- name: patient-db
url: tcp://database.internal:5432
upstream:
url: tcp://192.168.1.101:5432 # PostgreSQL database
- name: clinic-dashboard
url: https://dashboard.internal
upstream:
url: http://192.168.1.102:3001 # Web dashboard
- name: agent-api
url: https://agent-api.internal
upstream:
url: http://localhost:4040 # Expose agent API internally
Now, we install and start the service:
ngrok service install --config /etc/ngrok.yml
ngrok service start
This starts all tunnels defined in the configuration file, ensures ngrok runs persistently in the background, and integrates with native OS service tooling.
When you reserve a TCP address**,** you can create a TCP cloud nendpoint that binds to that domain. You can reserve TCP addresses with ngrok’s pay-as-you-go plan.
curl -X POST \
-H "Authorization: Bearer <NGROK_API_KEY>" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{"description":"Patientdb Address", "region":"us"}' \
https://api.ngrok.com/reserved_addrs
Creating a custom wildcard domain will allow you to create endpoints and receive traffic on any subdomain of your domain. It can be helpful to create a separate subdomain for each customer site you wish to connect to. You can utilize wildcard domains on ngrok’s pay-as-you-go plan—reach out to support to get access.
curl \
-X POST \
-H "Authorization: Bearer {API_KEY}" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{"domain":"*.api.clinicnet.com","region":"us"}' \
https://api.ngrok.com/reserved_domains
Create a bot user so that you can create an agent authtoken independent of any user account. A bot user represents a service account, and allows each customer network to have its own authtoken. In the case one authtoken is compromised, only that customer network may be affected rather than all of them.
curl \
-X POST \
-H "Authorization: Bearer {API_KEY}" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{"name":"bot user for clinic 1"}' \
https://api.ngrok.com/bot_users
Navigate to the ngrok dashboard under *Authtokens` and create an authtoken for that bot user and assign it an ACL Binding, so that it can only create endpoints bound to the reserved wildcard domain for that clinic’s rest api.
Since the REST API and database must be always accessible, we create permanent cloud endpoints that route traffic to their internal endpoints. First, create an HTTPs cloud endpoint for the API using the ngrok platform API.
curl -X POST \
-H "Authorization: Bearer <NGROK_API_KEY>" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{
"url": "https://clinic1.api.acme.com",
"type": "cloud",
"traffic_policy": {
"on_http_request": [
{
"expressions": [{ "req.url.path.startsWith": "/api" }],
"actions": [{ "type": "forward-internal", "config": { "url": "https://api.internal" } }]
}
]
}
}' \
https://api.ngrok.com/endpoints
Now, clinic1.api.clinicnet.com/api
permanently forwards traffic to https://api.internal
inside the factory.
Next, create a TCP cloud endpoint for the patient database using the ngrok platform API to route traffic to the https://database.internal
internal TCP endpoint you already created.
curl -X POST \
-H "Authorization: Bearer <NGROK_API_KEY>" \
-H "Content-Type: application/json" \
-H "Ngrok-Version: 2" \
-d '{
"url": "<RESERVED_TCP_ADDRESS>",
"type": "cloud",
"bindings": ["public"],
"traffic_policy": {
"on_tcp_connect": [
{
"actions": [
{
"type": "forward-internal",
"config": {
"url": "tcp://database.internal:5432"
}
},
{
"type": "restrict-ips",
"config": {
"enforce": true,
"allow": [
"203.0.113.0/24"
],
"deny": [
"192.0.2.0/24"
]
}
}
]
}
]
}
}' /
https://api.ngrok.com/endpoints
Now your reserved TCP address forwards TCP connections to the factory database and only allows access from 203.0.113.0/24
and denies traffic from 192.0.2.0/24
using the restrict-ips
Traffic Policy action.
Since the web dashboard should only be online when needed, we use ngrok’s agent API to dynamically start and stop tunnels.
As a technician, you can start a tunnel with an API request:
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"name": "dashboard",
"proto": "http",
"addr": "3001",
"domain": "dashboard.clinic1.com"
}' \
https://agent.example.com/api/tunnel
Now, dashboard.clinic1.com is live—only for this session.
Navigate to your newly created REST API Cloud Endpoint in the endpoints tab on the dashboard, and apply a Traffic Policy.
Make sure you have a certificate on hand or generate them using the instructions in our terminate-tls
docs.
on_tcp_connect:
- actions:
- type: terminate-tls
config:
mutual_tls_certificate_authorities:
- |-
-----BEGIN CERTIFICATE-----
... certificate ...
-----END CERTIFICATE-----
on_http_request:
- actions:
- type: oauth
config:
provider: google
- type: forward-internal
config:
url: https://api.internal:443
You’ve now got a zero-VPN setup that:
This architecture gives you full control without the complexity of VPNs or clunky networking gear. It’s reliable. It's flexible. And your customers will never need to think about it (which is the point).
Next steps:
Let’s get your next clinic—or factory, or warehouse, or quirky IoT setup—online without a VPN in sight.