# Fix Rails “Blocked hosts” through a tunnel

> Fix Rails “Blocked hosts”: allow the tunnel in config.hosts, then trust the exact qURL browser origin for development form posts. Keep CSRF tokens enabled.

Source: https://layerv.ai/guides/fix-rails-blocked-hosts/

---

[← All guides](https://layerv.ai/guides/) 

# Fix Rails “Blocked hosts” through a tunnel

Content updated October 25, 2026.

## Why does Rails say “Blocked hosts” when I open my app through a tunnel?

Rails rejects unknown hosts with “Blocked hosts”. Add .qurl.site or your tunnel hostname to config.hosts. For qURL™ form posts, allow the exact browser origin in development while retaining origin and authenticity-token checks. With qURL, the app stays on your machine with no public URL, and qurl stop ends access whenever you want. If you later need one permanent public hostname, ngrok’s dev domain fits.

## What you need

- A Rails app running with bin/rails server in development, opened through a tunnel or qURL link
- config/environments/development.rb

## Steps

1. ### Read the host from the error  
Rails answers 403 with a page that says “Blocked hosts: c-k3j2ab.qurl.site” and suggests the line to add, config.hosts << "c-k3j2ab.qurl.site". Through qURL the host can appear twice, because Rails checks X-Forwarded-Host as well as Host. The log shows the same list from ActionDispatch::HostAuthorization.
2. ### Add the tool’s domain to config.hosts  
Add the domain, not the one hostname the error names, so the next link works too. In Rails a leading dot allows the domain and one more label in front of it: .qurl.site covers c-….qurl.site, but .ts.net does not cover machine.tailnet.ts.net, so use .tailnet.ts.net or a regular expression there. Your entries add to the development defaults, so localhost keeps working. Restart the server.  
```  
# config/environments/development.rb
Rails.application.configure do
  config.hosts << ".qurl.site"
  # ngrok: "your-domain.ngrok-free.app"   Cloudflare Quick Tunnel: ".trycloudflare.com"
end
```
3. ### Or allow it for one run without editing a file  
The development default reads RAILS\_DEVELOPMENT\_HOSTS, a comma-separated list, when the server starts.  
```  
RAILS_DEVELOPMENT_HOSTS=.qurl.site bin/rails server
```
4. ### Share it through qURL  
Publish the running dev server, make a link, and send it to the recipient. They open it and keep that page open while you configure their exact browser origin in the next step. The first publish creates this device’s identity and needs no account. Each link opens once and expires within 24 hours.  
```  
qurl publish http://127.0.0.1:3000
qurl share <CRID>
```
5. ### qURL: trust the exact browser origin in development  
Rails compares the browser’s Origin with the URL it believes it is serving. Through qURL the browser uses https://r\_….qurl.site while Rails sees http://c-….qurl.site. Keep forgery\_protection\_origin\_check enabled and add the development-only exception below at the end of ApplicationController. Ask the recipient to send you the https origin from the address bar of the page they just opened. They must keep that page open; do not open their single-use link yourself. Set QURL\_DEV\_ORIGIN to that exact origin, restart Rails, and ask them to reload the same page. This configuration trusts one recipient origin at a time; update it for each new browser session. Trust only that exact address, never a whole-domain match, which would also trust other publishers’ links. Keep cookie domains host-only.  
```  
# app/controllers/application_controller.rb (inside ApplicationController)
private def valid_request_origin?
  trusted_origin = ENV.fetch("QURL_DEV_ORIGIN", "")
  if Rails.env.development? &&
      request.host.end_with?(".qurl.site") &&
      trusted_origin.start_with?("https://") &&
      request.origin == trusted_origin
    true
  else
    super
  end
end
# Restart with the exact browser origin (replace this example):
# QURL_DEV_ORIGIN=https://r_your-link.qurl.site bin/rails server
```

## What the recipient sees

With the host allowed and the exact browser origin trusted in development, pages load through the link and forms post normally.

## Which tool should carry your Rails dev server?

| Option                                                                                  | Choose it when                                                                                                                                                  | Trade-off                                                                                                                   |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [qURL](https://layerv.ai/cli/)                                                          | qURL fits when particular people should try the Rails app: one .qurl.site entry in config.hosts covers every qURL link, with no account.                        | For qURL in development, trust the link’s exact browser address with QURL\_DEV\_ORIGIN; the CSRF token check stays on.      |
| [ngrok](https://layerv.ai/compare/qurl-vs-ngrok/)                                       | ngrok fits when you want one stable URL: add your ngrok dev domain to config.hosts once, and form posts pass the origin check unchanged.                        | ngrok needs an account and authtoken, the URL is public, and free-plan browser visitors see a warning page first.           |
| [Cloudflare Quick Tunnels](https://layerv.ai/compare/qurl-vs-cloudflare-quick-tunnels/) | A Cloudflare Quick Tunnel fits a no-account look: .trycloudflare.com covers the hostname that changes every run.                                                | A Cloudflare Quick Tunnel URL is public unless you set --allowed-mail, and Quick Tunnels do not support Server-Sent Events. |
| [Tailscale Funnel](https://layerv.ai/compare/qurl-vs-tailscale-funnel/)                 | Tailscale Funnel fits when the machine is already on a tailnet: allow its tailnet domain, such as .tailnet.ts.net, since a bare .ts.net is too short for Rails. | Tailscale Funnel makes the Rails server public to anyone on the internet, with no access control on Funnel traffic.         |

## Troubleshooting

| If you see                                                                                                                                      | Do this                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ActionController::InvalidAuthenticityToken: “HTTP Origin header (https://r\_….qurl.site) didn’t match request.base\_url (http://c-….qurl.site)” | The origin check failed, not the token. Keep forgery\_protection\_origin\_check enabled, add the exact-origin override above, set QURL\_DEV\_ORIGIN to the browser’s https origin, and restart Rails. If the origin changes, update that value.                                                                                                                                                                                                               |
| A .ts.net or other leading-dot entry still shows “Blocked hosts”                                                                                | In Rails a leading dot matches at most one label in front of the domain. Add the longer domain, such as .tailnet.ts.net, or a regular expression such as /.\*\\.ts\\.net/.                                                                                                                                                                                                                                                                                    |
| qurl publish says the local app is not accepting TCP connections                                                                                | Start the app first and check that curl works on the same address. qurl publish connects to localhost as 127.0.0.1, so if your app listens only on IPv6 (on macOS or Linux, lsof -iTCP -sTCP:LISTEN -nP shows \[::1\]), publish 'http://\[::1\]:PORT' instead, or start the dev server with --host 127.0.0.1 and publish that address. Neither requires listening on your LAN. Keep the quotes: zsh treats the brackets as a pattern.                         |
| The recipient sees “Access link invalid”                                                                                                        | The link was already used or has expired. Run qurl share again and send the new link.                                                                                                                                                                                                                                                                                                                                                                         |
| The recipient sees “Resource not found”                                                                                                         | The resource was deleted after the link was made. Publish the app again, then run qurl share and send the new link.                                                                                                                                                                                                                                                                                                                                           |
| publish rejects http://127.0.0.1 with “only HTTPS URLs are allowed”, or asks you to log in                                                      | You have a CLI older than 3.0\. On macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. After updating, qurl --version should print 3.4.1 or later.                                                                                                                                                                                                                                                                     |
| publish prints no Access: line, or the Access: line says public and you did not ask for public                                                  | With no Access: line, the CLI is older than 3.4.1: on macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. A resource that an older CLI published is public unless you published it with --private, and anyone who has the CRID of a public resource can open it. To make a public resource private, run qurl delete <CRID> and publish again with qurl 3.4.1 or later. The new resource is private and has a new CRID. |
| publish says this Linux session “can’t reach a systemd user manager”                                                                            | You are in a container or sandbox without a user service manager. Run qurl publish with --foreground; sharing lasts as long as that process runs.                                                                                                                                                                                                                                                                                                             |
| publish says “Quota Exceeded” with “protected resource limit reached”, or “Your qURL account has reached a plan limit”                          | You can see this without an account: a device with no account can keep up to 3 active resources, with links that last up to 24 hours. Run qurl list --status active, then qurl delete <CRID> for any you no longer need (it asks you to confirm; --yes skips that). qurl stop keeps the resource, so it does not free a slot.                                                                                                                                 |

## Frequently Asked Questions

Why use an exact origin instead of a qurl.site suffix check? 

A suffix check would also trust pages run by other qURL publishers. The override above accepts only the exact QURL\_DEV\_ORIGIN in development and otherwise calls super. It rejects another qURL origin, an unrelated site, a lookalike suffix and null; the CSRF token is still required. Do not disable forgery\_protection\_origin\_check or use this development exception in production.

Why does the qURL host appear twice in the error? 

Rails checks both the Host header and X-Forwarded-Host, and a qURL link sets both to c-….qurl.site. Both must be allowed; one .qurl.site entry covers both.

Can I turn host authorization off? 

Rails lets you clear config.hosts, but the check exists because of DNS rebinding attacks, which Rails 6.0 added it to guard against. Add the domains you need instead.

Who else can reach my app while the link is open? 

Opening the link admits the network address it was opened from for the session, which lasts 1 hour by default. Other devices that share that public IP address, such as people on the same office or café network or VPN, can reach the app until the session ends. By default a link admits whoever opens it; access rules set through the API, such as IP allow and deny lists, can restrict that. Keep your app’s own sign-in for anything sensitive.

## Related Resources

[Guide: Allow a tunnel’s hostname in your dev server →](https://layerv.ai/guides/allow-tunnel-hostname-in-dev-server/) [Guide: Fix Django “Invalid HTTP\_HOST header” →](https://layerv.ai/guides/fix-django-disallowedhost-invalid-http-host/) [Guide: Share a dev server with a client →](https://layerv.ai/guides/share-dev-server-with-client/) [Rails guide: config.hosts ↗](https://guides.rubyonrails.org/configuring.html#config-hosts) [Rails guide: forgery\_protection\_origin\_check ↗](https://guides.rubyonrails.org/configuring.html#config-action-controller-forgery-protection-origin-check)

## Publish your app now

Install the CLI, then run one command. No sign-up, no API key.

[Install the CLI ↗](https://layerv.ai/cli/)



---

*This markdown version is auto-generated from [https://layerv.ai/guides/fix-rails-blocked-hosts/](https://layerv.ai/guides/fix-rails-blocked-hosts/) for AI agents. Curated agent resources: [llms.txt](https://layerv.ai/llms.txt). For the full interactive experience, visit the HTML version.*

