The dev container image generated a TLS keypair at build time and
shipped it inside the image, so every pull of the same image tag was
serving the same private key. The entrypoint also reverted to USER 0
to support a dead `createusers.txt` loop and a `chmod u+s` that was a
no-op (set on a binary owned by 1001). Net result was that any RCE
in the gateway landed as root and the wire-trust posture relied on a
shared private key.
Stop generating the cert at build time: the runtime image now carries
openssl and the entrypoint mints an ephemeral self-signed cert at
first start when no cert is mounted at the configured path. Each
container instance gets its own key. Drop USER 0 entirely; the
entrypoint runs as 1001 throughout. Prune the dead createusers loop
and the `chmod u+s`.
Separately, the README and the dev compose files publish a small set
of literal placeholder values for SessionKey, SessionEncryptionKey,
and the various Token*Key fields. Operators copy-paste these into
real deployments. Refuse to start when any of those literals appear
in the corresponding config field.
The auth daemon's gRPC socket was world-writable and accepted any
local UID that could connect to it. On a multi-tenant host any user
on the box could speak the gRPC API and run an arbitrary username/
password through PAM -- effectively an unauthenticated PAM oracle.
Create the socket with mode 0660 (Umask(0117)) and gate Accept on
SO_PEERCRED: only the daemon's own UID is allowed by default, plus
any operator-supplied --allow-uid / --allow-gid. Privilege-separated
deployments (rdpgw and rdpgw-auth as different users) need to list
the gateway's UID, or share a group; the existing path otherwise
would have been permissive.
The peer-credentials check is Linux-only; the non-Linux build keeps
the listener as-is and logs a warning, since rdpgw-auth itself
requires libpam and is effectively Linux-only in practice.
EnrichContext used to copy the first X-Forwarded-For entry into the
request identity unconditionally. The resulting AttrClientIp drives
client-IP comparisons later in the gateway-access flow, and a direct
caller could set XFF to anything they liked.
Add a small package-level allow-list:
* InitTrustedProxies(cidrs) parses operator-supplied CIDRs once at
startup. A bad CIDR is fatal, an empty list disables XFF entirely.
* EnrichContext takes the client IP from r.RemoteAddr (host portion)
and only swaps in the first X-Forwarded-For entry when r.RemoteAddr
itself sits in a trusted-proxy CIDR. AttrProxies is set from the
remaining XFF entries on the same condition.
Wire Server.TrustedProxies through configuration.go to web.
The `roundrobin`, `signed`, and `unsigned` host-selection modes route
requests against an operator-curated `Server.Hosts` list. The `any`
mode does not -- it forwards to whatever `?host=` value the request
carries, which makes the gateway usable as a generic TCP relay
against whatever the gateway can reach (loopback, RFC1918, link-local,
the cloud metadata service, arbitrary high-numbered ports on public
hosts).
Add a small destination policy applied only in `any` mode:
* Reject hosts that resolve to loopback, RFC1918, IPv6 ULA,
link-local, unspecified, or multicast addresses. Operators can opt
back in with `Server.AllowPrivateDestinations: true`.
* Restrict the destination port to `Server.AllowedDestinationPorts`
(default {3389}).
The other host-selection modes are unaffected -- the operator already
curates their hosts list.
The DestinationPolicy zero value is the secure default, so direct
&Handler{} constructions in tests still get the expected behavior.
DNS names are resolved at validation time and every returned address
is checked.