When an external service needs to send a webhook to an application running on your laptop, the problem can look purely technical: somehow, you need to make localhost:3000 reachable from the internet.
Port forwarding, a public tunnel, or a server with a public IP can all provide that reachability. But the same route that carries the webhook can also carry automated scans, malformed requests, unwanted traffic, and attempts to exploit weaknesses in the local service.
The more useful question is not simply, “How do I expose localhost?” It is:
Where should public traffic terminate, and which component should take responsibility for the request?
To answer it, we will compare four approaches:
port forwarding;
raw tunnels;
direct exposure;
a controlled relay model.
All four may look similar to the webhook provider: each gives it a public URL. The important difference is where the boundary between the public internet and your internal application actually sits.
You are not really exposing localhost
localhost is a loopback address. It only exists from the perspective of the machine itself. When people talk about exposing localhost, what they really mean is creating a public route to a process listening on a local port:
Internet → public address or intermediary → local port → application
That distinction matters. The word localhost sounds private and isolated, but once the route exists, the application can receive input from an untrusted network. Its code, dependencies, utility endpoints, and resource limits all become part of the external attack surface.
Opening one port does not automatically expose the entire computer. A carefully scoped firewall rule, strong authentication, and a hardened reverse proxy can reduce the risk considerably. There is still a new entry point, though, and someone has to secure, monitor, and maintain it.
Port forwarding: a direct route through the network boundary
With port forwarding, a router or firewall sends traffic from a public IP address and port to a machine on the private network:
Internet → public IP:443 → NAT / firewall → 192.168.1.20:3000
The route is straightforward, and you retain full control over the infrastructure. If your team already operates a static public IP, firewall policy, TLS termination, monitoring, and a reliable patching process, port forwarding can be a deliberate and reasonable choice.
For local development, however, it often creates more operational work than expected:
you need a public address or a separate answer to CGNAT;
an inbound firewall rule becomes part of the network's persistent configuration;
DNS, TLS certificates, and the reverse proxy remain your responsibility;
availability depends on the machine, the local connection, and the path through NAT;
an overly broad rule can expose the wrong interface or service;
automated scanners can find a public port whether or not you have advertised it.
The defining property of port forwarding is that an external client opens a connection along a route that ends inside your network. Your infrastructure is responsible for filtering and safely terminating that connection.
Raw tunnels: easier setup, often the same trust model
A tunnel usually starts with a local agent. The agent establishes an outbound connection to a public service, which gives you a URL and forwards incoming traffic to the local port:
Internet → public tunnel URL ⇄ local agent → localhost:3000
This is much easier than configuring port forwarding. You do not need a public IP, you do not have to change NAT rules, and HTTPS is often included. For a short debugging session, a live demo, or a quick integration test, a tunnel may be exactly the right tool.
The direction of the agent's connection does not, by itself, make the local application private. If anyone who can reach the public URL can send an HTTP request that is immediately forwarded to the local port, the internet still has a logical route to the handler. The mechanism for crossing the network boundary has changed; the trust model may not have.
A basic tunnel also tends to couple public availability to the state of the local machine. If the laptop sleeps, its network drops, or the tunnel process stops, there is nowhere to forward the request. Unless the product provides persistence and deferred delivery as separate features, the webhook is now dependent on the sender's retry policy.
Tunnel products vary widely. Authentication in front of the endpoint, IP allowlists, route restrictions, HTTP inspection, and access policies can make a substantial difference. The risk is not determined by the word “tunnel.” It depends on the actual configuration and on whether an external client can directly initiate a request to the local application.
Direct exposure: the application itself becomes public
In a direct-exposure model, the handler runs on a public machine or listens on a public interface:
Internet → public application server → webhook handler
This is a normal production architecture when a proper edge layer protects the application: a reverse proxy or ingress, TLS, firewall rules, request-size and rate limits, hardened application settings, regular updates, and monitoring.
The risky version is putting a local development server on the internet without those controls. A development environment may include:
verbose stack traces and debug pages;
hot reload and framework utility endpoints;
test data and relaxed authentication;
plugins or dependencies that were never intended to handle untrusted traffic;
handlers without rate limiting or body-size limits;
neighboring routes that nobody intended to publish.
Even the statement “we only exposed the webhook endpoint” needs to be verified. If /debug, /metrics, an administrative API, and ordinary application routes are served by the same process, the entire HTTP server may now be reachable—not just one handler.
Direct exposure can be secure. Its security comes from the design and operation of the complete public system, not merely from putting HTTPS in front of a development server.
Controlled relay: separate public ingress from local delivery
In a controlled relay model, the public URL is not a direct doorway to the local port. A continuously available ingress layer accepts the HTTP request, applies its own limits, and records it first. An authorized local agent then receives the request over an outbound connection and delivers it to a specific handler.
External service
→ public ingress
→ stored request
→ managed delivery
→ outbound connection from a local agent
→ local handler
The external service communicates with the public ingress layer. It does not open an arbitrary connection to the local HTTP server. Public ingress and private delivery are separated not only at the network level, but also by an explicit delivery state.
This model can:
keep the local or internal service behind the firewall, without a public IP or inbound port;
accept requests while the local machine is temporarily unavailable;
deliver only to a preconfigured Destination instead of allowing external clients to open arbitrary connections into the private network;
separate the HTTP response returned to the sender from the later delivery result;
retain the request and attempt history for diagnosis;
control retries and delivery rate independently of the sender's policy.
The word controlled is important here. This is not just an intermediary moving bytes in both directions. It is an application-level delivery system that knows a Request was accepted, where it should go, and how each delivery attempt ended.
Comparing the four models
| Model | Where the public connection terminates | Can an external request immediately reach the local process? | What happens while the local machine is unavailable? | Main operational responsibility |
|---|---|---|---|---|
| Port forwarding | At your network boundary and internal service | Yes | The connection fails or times out | NAT, firewall, TLS, hardening, availability, and logs |
| Raw tunnel | At the tunnel provider, before traffic is forwarded locally | Usually, while the tunnel is active | Product-dependent; without persistence, forwarding is impossible | Endpoint access, tunnel policy, and local-service security |
| Direct exposure | At the public application or its reverse proxy | The application is already public | Depends on the public deployment's availability | The complete public edge and application-security stack |
| Controlled relay | At a separate public ingress layer | No arbitrary direct connection; an authorized agent performs delivery | An accepted request can wait for delivery, subject to retention and configuration | Trust in the relay, its configuration, agent credentials, and handler security |
The key point is that a public URL tells you very little about the trust boundary. Two services may both use an outbound connection from a local agent. One can transparently forward public traffic into the process, while the other first accepts a request as an event and then starts a controlled delivery operation.
Risks that are easy to underestimate
A development server starts receiving untrusted traffic
A local server is normally run with the assumption that its clients are the developer, a test suite, or a frontend on the same machine. Publishing it invalidates that assumption. An obscure URL is not an access-control mechanism: it can leak through provider configuration, logs, shell history, screenshots, or browser history, and it may still be discovered by automated scans.
Webhook availability now depends on a laptop
If the public endpoint merely forwards a live connection to a local process, closing the laptop, restarting the application, or losing Wi-Fi becomes an externally visible outage. The sender sees a timeout or connection error and follows its own retry policy. You may have little control over the number of attempts, their timing, or how long the event remains recoverable.
Network reachability is mistaken for authorization
A request reaching the right port—or arriving through a hard-to-guess URL—does not prove who sent it. The handler must still verify the provider's signature, timestamp, and any other authentication mechanism defined by the integration. Static secrets in URLs also need careful handling because URLs are commonly copied into logs and history.
One expensive request can affect the development machine
A large body, a slow connection, or a burst of requests can consume memory, CPU time, file descriptors, and application connections. Without limits at the public boundary, even legitimate provider retries can disrupt local development. Hostile traffic makes the same problem worse.
A route exists, but a delivery record does not
Port forwarding answers “How do I pass this connection through?” It does not necessarily answer “Was the request accepted?”, “What exactly arrived?”, “How many attempts were made?”, or “Can I safely try delivery again?” After a failure, the evidence may be scattered across the sender, tunnel, proxy, and application logs.
How Adal Cloud draws this boundary
With Adal Cloud, an external service sends its webhook to the permanent HTTPS URL of a Server in Adal. The Server accepts the Request in the selected region and, when storage is enabled, retains it for the applicable retention period. Adal CLI, running on a developer machine or inside a private network, establishes a secure outbound connection and delivers the Request to the configured Destination.
External service
→ Server in Adal
→ Request
→ Delivery
→ Adal CLI
→ localhost or a service in a private network
The sender does not connect to Adal CLI and does not choose the local address. It only communicates with the public Server in Adal. The Destination owner configures the local URL, and delivery takes place through the CLI's authorized connection.
Separating ingress from delivery does more than reduce the internal service's direct attack surface. The Request is visible in the dashboard, and the result of each Delivery remains distinct from the fact that the Request was accepted. Configured retries apply when the handler is temporarily unavailable. If the CLI is disconnected, pending Requests can be delivered after it reconnects when Deliver pending requests on connect is enabled and the Requests are still within retention. See the documentation for storage and retention and retries for the relevant behavior and limits.
You can also use Minimum delivery interval to pace requests sent to the internal service. This helps smooth bursts between Adal Cloud and the Destination, but it does not replace signature verification or application-level authorization.
A controlled relay does not make the handler trusted
Removing a public inbound port reduces the internal service's direct external attack surface. It does not turn webhook input into trusted data. The handler still receives data that originated on the internet, only through a controlled delivery chain.
You still need:
webhook signature verification according to the provider's specification;
validation of the method, path, headers, content type, and payload schema;
limits on request size and processing time;
idempotent handling of duplicate deliveries;
secure storage of the local agent's token;
least-privilege execution for the receiving process;
an intentional retention policy for webhook data.
Adal Cloud does not promise exactly-once delivery. If the handler completes an operation but its response is lost, the Request may be delivered again. Use the provider's stable event ID, when available, or another idempotency key to prevent a duplicate payment, order, or notification.
A relay also becomes a service you have to trust. Its data location, retention behavior, credential model, operating limits, and reliability all matter. The architecture reduces direct exposure; it does not eliminate the need to evaluate the component at the new public boundary.
Choosing the right approach
The right model depends on the job, not on how quickly you can get a public URL.
Port forwarding makes sense when you intentionally operate the network boundary and are prepared to own its firewall, TLS, hardening, monitoring, and availability.
A raw tunnel can be ideal for a short-lived debugging session or live demo. Before using one, check who can reach the public endpoint, whether authentication and allowlists are available, which local routes become reachable, whether the address changes, and what happens when the tunnel disconnects.
Direct exposure is appropriate for an application designed and operated as a public service. A development server does not become production-ready simply because it sits behind HTTPS.
A controlled relay fits situations where the webhook provider needs a stable public endpoint but the receiving application should remain inside a private network. It is particularly useful when requests need to be accepted independently of local availability, retained, and delivered with a visible attempt history.
The deciding question is:
Does the external service need a direct route to the application, or does it only need a reliable place to hand off an event?
For webhooks, the second model is often the closer match. A sender needs a stable public HTTPS endpoint. It does not necessarily need a path that ends at your laptop.