MitchDrage opened a new issue, #13720:
URL: https://github.com/apache/cloudstack/issues/13720
### problem
SUMMARY
On a redundant VR, the password server is started with both the VRRP VIP and
the router's own guest IP, but passwd_server_ip.py binds only the first of the
two. Guests derive the password server address from the DHCP server-identifier,
which is the router's own guest IP, so their requests to port 8080 are answered
with a TCP RST. Passwords accumulate uncollected in
/var/cache/cloud/passwords-<vip> and password injection silently fails for
every guest on every isolated network with redundant routers.
This is not to be confused with #12161, which changes the DNS list for
redundant VPC VRs only and does not affect this path.
CODE
I believe this was introduced in #10183 when a second entry became added to
listen address list for the real IP of the VR unconditionally. The password
server script chooses addresses[0], so never selects the real IP in a redundant
VR deployment, but that is what the DHCP server tells the client to use.
First released in 4.19.2.0, still present on main.
Code:
passwd_server_ip.py serve() Lines 179-198 (4.22.0.0) binds addresses[0];
CsDhcp.py configure_server() Lines 145-147 builds the listen-address list.
In the environment below, the guest network is 192.168.0.0/24, the VRRP VIP
(gateway) is 192.168.0.1, and the primary router's own guest IP is 192.168.0.48.
EXPECTED RESULTS
The guest retrieves its password from the address it was given as the DHCP
server-identifier, and the entry is removed from the password cache.
ACTUAL RESULTS
The password server listens on the VIP only:
root@r-3050-VM:~# ss -lntp | grep 8080
LISTEN 0 5 192.168.0.1:8080 0.0.0.0:* users:(("python",pid=3372,fd=3))
Passwords are delivered by the management server and never collected:
root@r-3050-VM:~# cat /var/cache/cloud/passwords-192.168.0.1
192.168.0.82=<redacted>
192.168.0.188=<redacted>
192.168.0.38=<redacted>
Metadata on port 80 works for every guest, because cloudstack binds all
addresses. Both agents complete a full metadata crawl and only the password
request fails. Note the 61 second stall in the cloudbase-init crawl, which is
the password request timing out:
192.168.0.82 - - [.../Jul/2026:03:30:36 +0000] "GET
/latest/meta-data/instance-id HTTP/1.1" 200 291 "-" "python-requests/2.33.1"
192.168.0.82 - - [.../Jul/2026:03:31:37 +0000] "GET
/latest/meta-data/service-offering HTTP/1.1" 200 302 "-"
"python-requests/2.33.1"
192.168.0.38 - - [.../Jul/2026:04:55:56 +0000] "GET
/latest/meta-data/instance-id HTTP/1.1" 200 291 "-"
"Cloud-Init/26.1-0ubuntu1~22.04.1"
192.168.0.38 - - [.../Jul/2026:04:55:56 +0000] "GET /latest/user-data
HTTP/1.1" 200 351 "-" "Cloud-Init/26.1-0ubuntu1~22.04.1"
The guest requests port 8080 at the router's own guest IP (192.168.0.48),
not at the VIP, and is reset because nothing is bound there:
05:03:16.482340 IP 192.168.0.38.43828 > 192.168.0.48.8080: Flags [S], seq
2121724, length 0
05:03:16.482361 IP 192.168.0.48.8080 > 192.168.0.38.43828: Flags [R.], seq
0, ack 2121725, win 0, length 0
05:03:17.484909 IP 192.168.0.38.43838 > 192.168.0.48.8080: Flags [S], seq
2008568524, length 0
05:03:17.484944 IP 192.168.0.48.8080 > 192.168.0.38.43838: Flags [R.], seq
0, ack 2008568525, win 0, length 0
This is not a firewall problem. The rules are present and counting the SYNs
above as accepted:
root@r-3050-VM:~# iptables-save | grep -E 'dport (80|8080)'
-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m state --state NEW -j ACCEPT
-A INPUT -s 192.168.0.0/24 -i eth0 -p tcp -m tcp --dport 80 -m state --state
NEW -j ACCEPT
-A INPUT -s 192.168.0.0/24 -i eth0 -p tcp -m tcp --dport 8080 -m state
--state NEW -j ACCEPT
17 204 10640 ACCEPT 6 -- eth0 * 192.168.0.0/24 0.0.0.0/0 tcp
dpt:8080 state NEW
Confirmed with a DNAT rule on the primary router, after which every pending
password was collected immediately:
iptables -t nat -I PREROUTING -i eth0 -d 192.168.0.48/32 -p tcp --dport 8080
\
-j DNAT --to-destination 192.168.0.1:8080
### versions
ACS: 4.22.1.0
systemvm template: systemvm-kvm-4.22.0-x86_64
Hypervisor: KVM.
Guests tested: Windows Server 2022 (cloudbase-init) and Ubuntu 22.04
(cloud-init 26.1). Same guest templates work correctly on a shared network
served by a single, non-redundant VR.
CONFIGURATION
Advanced zone, KVM, isolated guest networks using a network offering with
redundant routers enabled. UserData/password service provided by VirtualRouter.
### The steps to reproduce the bug
1. Create an isolated network using an offering with redundant VRs enabled
and UserData provided by VirtualRouter.
2. Deploy a guest from any template with a password-enabled image
(cloudbase-init or cloud-init), or reset the password of an existing guest.
3. Observe the guest never receives its password.
### What to do about it?
passwd_server_ip.py: start a listener for every address supplied rather than
only addresses[0]. allowAddresses already contains both, and savepassword.sh
already POSTs to every local IP, so no other change is needed. Keeping
addresses[0] as listeningAddress preserves the existing
/var/cache/cloud/passwords-<vip> path. This is what I am running locally using
custom userdata, verified across save, fetch and acknowledge.
Something like this??
FROM:
(Starting at line 182 of passwd_server_ip.py)
```
global listeningAddress
global allowAddresses
if len(sys.argv) > 1:
addresses = sys.argv[1].split(",")
if len(addresses) > 0:
listeningAddress = addresses[0]
allowAddresses.append(addresses[0])
if len(addresses) > 1:
allowAddresses.append(addresses[1])
server_address = (listeningAddress, 8080)
passwordServer = ServerClass(server_address, HandlerClass)
passwordServer.allow_reuse_address = True
sa = passwordServer.socket.getsockname()
initToken()
loadPasswordFile()
syslog.syslog('serve_password running on %s:%s' % (sa[0], sa[1]))
try:
passwordServer.serve_forever()
```
TO:
```
global listeningAddress
global allowAddresses
addresses = [listeningAddress]
if len(sys.argv) > 1:
addresses = [a.strip() for a in sys.argv[1].split(",") if a.strip()]
listeningAddress = addresses[0] # Need to keep listeningAddress
as-is on the first address as it's global. Used in getPasswordFile()
for addr in addresses:
if addr not in allowAddresses:
allowAddresses.append(addr)
initToken()
loadPasswordFile()
servers = []
for addr in addresses:
server = ServerClass((addr, 8080), HandlerClass)
server.allow_reuse_address = True
servers.append(server)
syslog.syslog('serve_password running on %s:8080' % addr)
for server in servers[1:]: # Add each subsequent server as a thread.
Primary server starts in the try block.
t = threading.Thread(target=server.serve_forever)
t.daemon = True
t.start()
try:
servers[0].serve_forever()
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]