An empty $_ENV array in a hook script feels like proof that OpenVPN passed nothing. It is usually proof of something else entirely: PHP was configured not to populate that superglobal, and the values were sitting in the process environment the whole time. The second false conclusion is subtler, because the two address variables in a connection hook mean different things and get used interchangeably. Both mistakes cost an afternoon if you start from the wrong one.

Two addresses, two different claims

The outer endpoint is what the server observed when the client connected, available as trusted_ip for IPv4 and trusted_ip6 for IPv6. The virtual address is what the pool assigned for this session, available as ifconfig_pool_remote_ip.

They answer different questions, and a record usually needs one of them rather than both. The first is where the connection arrived from, which may be a shared NAT address used by hundreds of people. The second is where this session lives inside the tunnel, and it can be reassigned to a different session later. Confusing the two produces records that look plausible and are wrong.

A minimal read looks like this.

$outer4 = getenv('trusted_ip');
$outer6 = getenv('trusted_ip6');
$inner  = getenv('ifconfig_pool_remote_ip');
$outer  = $outer4 !== false ? $outer4 : $outer6;

Neither value identifies a person. One is a network observation, the other a lease, and a record that treats either as a stable device identifier will be wrong the first time a client reconnects from a different network or receives a different pool address. Choose the field by the question the record must answer, then write that question beside the column.

Check how PHP was launched, not your shell

The hook runs as a child of the VPN daemon with an environment the daemon constructs. Your interactive shell has a different one, and running the script by hand tests the shell's environment rather than the hook's.

Start by confirming the hook actually fired at the stage you expected. A script attached to the wrong stage, or one that never ran because the directive points at a different path, produces exactly the same symptom as a missing variable. Add a deliberate marker line at the top of the script and confirm it appears before you inspect anything else. A single timestamped line in the log separates never ran from ran without the value.

Then confirm which interpreter and which configuration file that invocation uses. Editing the PHP configuration for a web server changes nothing about a CLI process started by OpenVPN, and the two can load entirely different php.ini files.

Why an empty $_ENV proves nothing

PHP's getenv reference documents a direct read of the process environment, which returns false when the name is absent. The $_ENV superglobal is populated separately, and the variables_order directive decides which superglobals get filled.

If that directive omits E, $_ENV stays empty no matter what the environment contains. Code that indexes it then reports a missing variable that is in fact present, and the investigation heads toward OpenVPN when the answer is in php.ini. Check the directive for the CLI configuration specifically, not for the web server.

Reading with getenv avoids the whole class of confusion, and a wrapper script that clears the environment before invoking the interpreter reintroduces it in a different form.

Store an association, not a row update

Decide which value the record needs before writing it. If the goal is to note where a session came from, store the outer endpoint and label it as an observation. If the goal is to map a session to a tunnel address for the duration of a connection, store the pool address and key it to the session.

Update by the authenticated identity and the current session rather than by every row sharing a tunnel address, because pool addresses get reused and a late disconnect event can otherwise overwrite a newer connection's data. Handle IPv6 explicitly instead of silently writing only the IPv4 value. A row with a NULL outer address is better than a row with the wrong one, because the first is visibly incomplete.

Key the write to the session identifier OpenVPN provides so a reconnect creates a new record rather than editing the old one. Use parameterized statements and an account with only the permissions the operation needs. A hook runs with the daemon's authority, so a careless query is a wider problem than a wrong log line.

Diagnose without dumping the environment

Log which names were present, which stage ran and which interpreter was used. Do not print the whole environment, because a hook process can inherit unrelated values that should not end up in a log file.

Verify one connection end to end: read the outer endpoint, read the assigned address, and confirm both against the server's own view of that session. Once the pair is confirmed for one client, the association can be trusted elsewhere.

Winndannde jowitiiɗe heen