Give a contractor access to one internal app, not your whole network
This guide is for teams who need to let a contractor use one web app without handing over VPN access to everything else. You will set up app-level access with your identity provider and a reverse proxy so the contractor can reach only the named app and nothing else on your private network.
TL;DR — The safest way to give a contractor access to one app is to publish only that app through an identity-aware proxy (a gate in front of the app that checks login and policy), not through a full-network VPN. The key move is: create a separate contractor group in your identity provider, allow that group on exactly one app hostname, and do not give them any route to your internal network. Reading time: ~5 min
Goal
When you are done, the contractor can sign in to one specific app URL, such as https://timesheets.example.com, and use only that app. They will not have VPN access, LAN access (your office/internal network), or access to any other internal apps.
Prerequisites
- Your identity provider admin login (IdP, the system that handles sign-in), such as Microsoft Entra ID, Google Workspace, or Okta
- Access to your DNS provider dashboard (where you manage DNS records), or to whoever manages DNS for your domain
- Access to the server or hosting setup that currently runs the app
- A public hostname you can use for the app, for example
timesheets.example.com - HTTPS/TLS (the browser padlock) already working for that hostname, or the ability to add it in your reverse proxy
- The contractor's email address
- A way to edit your reverse proxy config, such as nginx, Traefik, Caddy, or your cloud access proxy dashboard
- If you use nginx on a Linux server: sudo access and nginx installed; check with:
nginx -v
Steps
Step 1: Pick one hostname for the app
Use one dedicated hostname for the app. Do not reuse a shared admin hostname.
Exact value to decide:
timesheets.example.com
What you should see when this succeeds: you have written down one app URL that is only for this app.
Step 2: Create a contractor group in your identity provider
In your identity provider's dashboard, create a group with this exact name:
contractor-timesheets
Typical menu path in your provider's dashboard:
Identity provider dashboard -> Groups -> Create group
Name: contractor-timesheets
Then add the contractor's user account to that group:
Identity provider dashboard -> Users -> [contractor email] -> Groups -> Add to group -> contractor-timesheets
What you should see when this succeeds: the contractor appears as a member of contractor-timesheets.
Step 3: Put an access gate in front of the app
Use your access proxy or reverse proxy to require login before the app loads. In many providers this is called an "application", "access app", or "protected resource".
Typical menu path in your access provider dashboard:
Access proxy dashboard -> Applications -> Add application
Hostname: timesheets.example.com
Type: Self-hosted web app
Policy: Allow group contractor-timesheets
If you run nginx yourself and your access layer passes the authenticated user in a header, use this nginx site config pattern:
server {
listen 443 ssl http2;
server_name timesheets.example.com;
ssl_certificate /etc/letsencrypt/live/timesheets.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/timesheets.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-User $http_x_forwarded_user;
}
}
What you should see when this succeeds: visiting https://timesheets.example.com redirects to your sign-in page instead of loading the app directly.
Step 4: Restrict access to exactly the contractor group
In the same access policy, set one allow rule and no broad allow rules.
Typical menu path:
Access proxy dashboard -> Applications -> timesheets.example.com -> Policies
Rule 1: Allow -> Group -> contractor-timesheets
Delete any rule like: Allow -> Everyone
Delete any rule like: Allow -> *@yourcompany.com
If your provider supports default action, set it to deny:
Default action: Deny
What you should see when this succeeds: the policy list shows only contractor-timesheets as allowed for this app.
Step 5: Point the app hostname at the access proxy
In your DNS provider dashboard, create the record for the app hostname. The exact target depends on your access provider. If your provider gave you a hostname like proxy.vendor.example, enter that exact value.
Typical menu path:
DNS provider dashboard -> DNS -> Add record
Type: CNAME
Name: timesheets
Target: proxy.vendor.example
TTL: Auto
If your provider requires an A record instead, use the IP they gave you:
Type: A
Name: timesheets
IPv4 address: 203.0.113.10
TTL: Auto
What you should see when this succeeds: the DNS record for timesheets.example.com appears in your DNS list.
Step 6: Remove any old network-wide access for the contractor
If the contractor already has VPN access, remove it now so they do not keep broad access after the app gate is live.
⚠️ If the contractor is actively working, removing VPN access can interrupt their current session. Do this after you confirm the app URL works through the new access gate.
Typical menu path:
VPN or network access dashboard -> Users -> [contractor email] -> Remove access
If access is controlled by a group, remove them from the VPN group:
Identity provider dashboard -> Users -> [contractor email] -> Groups -> Remove from vpn-users
What you should see when this succeeds: the contractor is no longer assigned to any VPN or broad network access group.
Step 7: Send the contractor the exact login URL
Send only the app URL, not server names, private IPs, or VPN instructions.
Copy this template:
Use this link to access the app: https://timesheets.example.com
Sign in with your work email address.
If you see an access denied message after login, reply with a screenshot of the full page.
What you should see when this succeeds: the contractor has one URL and no network credentials.
Verify it works
Check the full flow with one allowed user and one blocked user.
Use this expected flow:
View as text
[Contractor Browser] ---- 1. GET app URL ----> [Access Proxy] [Access Proxy] ---- 2. redirect to login ----> [IdP] [IdP] ---- 3. successful login ----> [Access Proxy] [Access Proxy] ---- 4. allow contractor-timesheets ----> [App]
From a browser:
Open https://timesheets.example.com
Expected result: you are redirected to sign in, then the app loads after login.
From a terminal, you can confirm the hostname answers over HTTPS:
curl -I https://timesheets.example.com
Expected result: one of these status lines appears first, depending on your access setup:
HTTP/2 302
or
HTTP/2 200
Then test with a user who is not in contractor-timesheets.
Expected result: after login, they see Access denied, Forbidden, or your provider's blocked page.
Finally, confirm the contractor has no VPN access by asking them to use the old VPN method. Expected result: login fails or no network route is available.
Common pitfalls
The contractor is still in a broad VPN or "all staff" group
Mistake: the user was added to the app group but not removed from a wider access group.
Symptom: they can still reach other internal apps or private IPs.
Fix: remove them from the VPN or broad-access group in Users -> [contractor email] -> Groups.
The app policy allows "everyone" as a second rule
Mistake: a default allow rule was left in place under the new contractor rule.
Symptom: anyone in your company can still open the app, or the contractor can access it even after group removal because another rule matches.
Fix: delete any rule like Allow -> Everyone and set Default action: Deny.
DNS points to the old app server instead of the access proxy
Mistake: timesheets.example.com still resolves to your original server, bypassing the login gate.
Symptom: the app opens directly with no sign-in prompt.
Fix: change the DNS record so timesheets.example.com points to the access proxy target, not the app server IP.
The app trusts any X-Forwarded-User header from the internet
Mistake: the app or proxy accepts identity headers without restricting who can send them.
Symptom: a direct request to the app can spoof a logged-in user.
Fix: expose only the access proxy publicly and keep the app listening on a private address like 127.0.0.1 or an internal network only.
The contractor signs in with the wrong email account
Mistake: they use a personal account instead of the account you added to contractor-timesheets.
Symptom: login succeeds but access is denied.
Fix: confirm the exact email address in the group membership and have them sign out, then sign in with that same address.
You test too quickly after changing DNS
Mistake: checking the new hostname before DNS caches refresh.
Symptom: one device shows the login page, another still reaches the old destination.
Fix: wait a few minutes, then test again in a private/incognito window and, if needed, from mobile data to avoid cached DNS.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI