Pointing a Subversion virtual host at a VPN address changes which site answers a request, not who is allowed to send one. The access check happens later, against the rules in the section that matched the URL, and a vhost name never enters that decision. If ServerName was your only change, the repository is exactly as reachable as it was yesterday. Two separate questions have to be answered in configuration: which socket accepted the connection, and which identity the request carried.

What ServerName actually decides

Name-based virtual hosting compares the Host header with ServerName and ServerAlias entries, picks one vhost, and lets that vhost handle the request. The connection itself was accepted earlier by a Listen socket that belongs to the server, not to the vhost. That is why a private-looking name offers no protection: any client that can reach the socket can send that Host header. The name is a routing key, and routing keys are chosen by the caller.

You can watch the mapping without guessing. apachectl -S prints each vhost, its port and its names, and marks which one is the default for the address.

RequireAll is the container that makes both checks mandatory

Apache 2.4 wraps the Require directives of a single section in an implicit RequireAny, so two bare directives mean either one is enough. Add the container and the meaning flips:

<Location /svn/repo>
    AuthType Basic
    AuthName "Subversion"
    AuthBasicProvider file
    AuthUserFile /etc/apache2/svn.htpasswd
    <RequireAll>
        Require ip 10.8.0.0/24
        Require valid-user
    </RequireAll>
</Location>

RequireAll needs both conditions: a source address inside the tunnel range and a user the repository accepts. That is what VPN-only should mean for a Subversion URL. The mod_authz_core reference documents the container rules. Keep the block inside the existing Location for the repository and leave the DAV directives that already work in place, because this fragment replaces neither AuthType nor the password file nor path-level authorization.

The source test comes from mod_authz_host, so confirm it is loaded before debugging anything else: apachectl -M | grep authz_host.

The address Apache evaluates is the socket address

Require ip compares against the address Apache recorded for the connection, which is not always the address the client believes it has.

When the VPN routes its addresses without translation, Apache sees the tunnel address and 10.8.0.0/24 matches. When a gateway translates, Apache sees the gateway and the rule quietly matches nothing; the same thing happens behind a load balancer that terminates TLS in front of the vhost. mod_remoteip exists for that case. RemoteIPHeader X-Forwarded-For together with RemoteIPTrustedProxy for the balancer replaces the recorded address, and only then does the range have something to compare against.

Trusting an arbitrary forwarding header is worse than having no rule at all. Any client can send X-Forwarded-For containing 10.8.0.9 and satisfy a range check that was never anchored to a trusted hop.

Layer the listener and the repository policy

Binding is a separate control and often the simpler one. Listen 10.8.0.1:443 decides which sockets exist, and a request cannot select a vhost on a socket that was never opened. The cost is startup order: if the tunnel interface is down when Apache starts, the bind fails and the service does not come up. Pair it with unit ordering, or bind to an address that exists independently of the tunnel.

Path permissions stay in AuthzSVNAccessFile. A valid account for the site is not the same grant as write access to a project path, and the network rule does not change that.

Watch section merging when the block sits in an included file. Apache merges authorization containers across nested sections and the most specific matching section can replace the requirement instead of adding to it, so a rule on /svn may not reach /svn/repo/trunk once a narrower section defines its own.

Prove both denials before you call it done

Test four requests, not one. Inside the VPN with a valid account you should get the repository listing. Inside the VPN with a wrong password you should get 401 rather than a listing. From outside the VPN with valid credentials you should get 403, and the same public request against an alternate hostname must fail the same way.

One command per case is enough: curl -sI -u user:pass https://svn.example.com/svn/repo/, then read the status line. Repeat the working case through the real client as well, because svn info exercises the DAV methods that commits and updates will use.

If any of the four results disagrees with the policy you intended, the configuration is not finished, and renaming the vhost will not change that.

Related articles