If a laptop whose antivirus signatures stopped updating weeks ago can still open your internal app, the policy document is seldom the thing that is wrong. Three separate systems have to agree before access is refused: something measures the endpoint, something converts that measurement into an allow or deny verdict, and something actually drops the traffic. When a non-compliant device gets in anyway, exactly one of those links is absent or is trusting evidence it should not. The checks below run in that order, and each one has a stopping point.

The block is the last step, not the first

Authentication and posture answer different questions. A successful login proves a credential was presented; it says nothing about the software running on the machine that presented it. That gap is why a copied client profile can carry a healthy-looking session onto an unhealthy host.

Endpoint compliance work already has a vocabulary for the chain. The informational NEA reference model in RFC 5209 splits the job into a collector, an assessment step and an enforcement point, and it states plainly that a client which lies about its own state is outside the model. That caveat matters more than the diagram: a trusted platform module does not turn a self-reported measurement into a fact.

Keep the collector honest about scope. It should report the attributes your policy names, on the device asking for access, and little else. An inventory agent that lists every installed package is a different tool from a posture collector, and the difference is both a retention problem and an accuracy problem. If the report cannot be tied to one device identity, the assessor is guessing about which machine it just cleared.

A dashboard showing a red status is not a control. Ask what a denied user can still reach: the wiki, the file share, the database port? Remediation needs one narrow surface, and everything else belongs behind the block.

Read the report before you read the rule

Start at the source instead of the client's green tick. On Windows the local measurement comes from Defender or from whichever product registered itself with the Security Center, and both are readable without a vendor console:

Get-MpComputerStatus | Select-Object AMServiceEnabled, RealTimeProtectionEnabled, AntivirusSignatureLastUpdated
Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct | Select-Object displayName, productState

The second command returns an integer called productState. Its bits are loosely documented and vendors pack the enable and up-to-date flags differently, so corroborate it rather than parse it. Third-party suites also leave the Security Center entry behind after uninstall, which reports on software no longer on disk.

Age is the field people skip. A signature timestamp only means something against the clock of the machine doing the assessment, so convert it to an interval instead of eyeballing it:

$sig = (Get-MpComputerStatus).AntivirusSignatureLastUpdated
[int](New-TimeSpan -Start $sig -End (Get-Date)).TotalHours

Now the policy has one number to read. If your rule says 72 hours, this is the number it uses, and a laptop asleep for a week fails without anyone interpreting a label. Record the threshold beside the command, because the two drift when the endpoint tool is replaced.

Turn each endpoint state into a written verdict

Most disputes about posture are really disputes about the unknown row. Decide it before the first incident, in a table that names the evidence, the verdict and the owner, so nobody improvises at the gateway console.

Evidence the assessor hasVerdictWhat it protects against
Timestamp inside the policy window, protective service runningAdmit to normal applicationsNothing yet; the other checks still apply
Timestamp older than the windowDeny, admit remediation page onlySignature rollback and dormant laptops
Service disabled or reporting offDeny, escalate to the endpoint ownerAn agent reporting stale success
No report at all for this sessionTreat as deny, never as healthyAgents that fail open under load

The bottom row is where products differ. Some gateways refuse the connection and explain why; others let the tunnel up and apply a restricted policy; a few fall back to the last known good report. Only the first two are defensible, and it is worth testing which one yours does.

Keep identity and permission in their own lanes as well. A laptop that passes every posture test still needs the right human authorization for the application, and a device certificate proves possession of a key rather than the state of the filesystem. Collapsing the three into one boolean is how a help-desk account ends up with standing access to production.

Check the enforcement path with a request, not a dashboard

Enforcement is a property of a request, so measure it with one. Put a non-compliant machine on the tunnel and make the call a real user would make:

curl -sS -o /dev/null -w '%{http_code}
' --max-time 5 https://app.internal.example/health

A 200 from a device that should be blocked means the decision never reached the data path. Two usual causes: the policy is attached to a group the test account never joined, or the application is reachable over a route the VPN does not own.

Check the routing assumption first. If the internal name resolves to an address the client already reaches over the local LAN, the tunnel is irrelevant and no policy will apply. Compare that address with the route the gateway pushes. On OpenVPN, per-client policy lives in files keyed by common name, so it is easy to inspect:

ls /etc/openvpn/ccd/
cat /etc/openvpn/ccd/laptop-17

A file containing disable disconnects that client outright, while a push "route ..." line narrows what it can reach. Both are enforcement, and both are visible to whoever is debugging the report.

Test the four cases on one scratch account

The final check is a small matrix, not a happy path. Drive one test account through four states: current, expired signature, protection disabled, agent not installed. For each, record the verdict in the assessment log, the HTTP status the app returns, and whether the remediation page loaded.

foreach ($case in 'current', 'stale', 'disabled', 'absent') {
  Get-MpComputerStatus | Select-Object AMServiceEnabled, AntivirusSignatureLastUpdated
  curl.exe -sS -o NUL -w "%{http_code}
" --max-time 5 https://app.internal.example/health
}

Two details are easy to miss. Replay first: a report captured while the machine was healthy must not be accepted for a later session on the same device, so look for a timestamp bound to the current connection. Then removal: uninstalling the agent should produce the same deny as a stale report, not a clean pass, and that is the case most often never tested.

When the matrix matches the table, hand three items to whoever runs the gateway next: the commands, the threshold, and the four expected outcomes. That handover is the real deliverable, because a posture design that lives only in a policy document drifts the first time the endpoint tool changes its reporting field.

Аффингæй хæстæг статьялтæ