vmware / pyvmomi

VMware vSphere API Python Bindings
Apache License 2.0
2.19k stars 766 forks source link

Support communication with vCenter Server via kerberos authenticated proxy sever #1043

Closed VedaNiks closed 1 week ago

VedaNiks commented 11 months ago

Is your feature request related to a problem? Please describe.

I need to communicate with vCenter Server and all the traffic goes through a kerberos authenticated proxy sever. I am not able to authenticate the proxy server using 'Proxy-Authorization' header. I am using below code:

import ssl
from pyVim.connect import SmartConnect, Disconnect
from requests_kerberos import HTTPKerberosAuth

def get_unverified_context():
    """
    Get an unverified ssl context. Used to disable the server certificate
    verification.
    @return: unverified ssl context.
    """
    context = None
    if hasattr(ssl, '_create_unverified_context'):
        context = ssl._create_unverified_context()
    return context

context = get_unverified_context()
kerb_auth = HTTPKerberosAuth(force_preemptive=True)
auth_header = kerb_auth.generate_request_header(None, '10.24.129.100', True)
custom_headers = {}
custom_headers['Proxy-Authorization'] = auth_header

si = SmartConnect(protocol='https',
                  host='10.24.129.1',
                  user='administrator@vsphere.local',
                  pwd='p@s$w0rD',
                  sslContext=context,
                  httpProxyHost='10.24.129.100',
                  httpProxyPort='3128',
                  customHeaders=custom_headers)

# Retrieve the service content
content = si.RetrieveContent()
vc_guid = content.about.instanceUuid
print(vc_guid)

I see below exception:

Traceback (most recent call last):
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVim/connect.py", line 491, in __Login
    content = si.RetrieveContent()
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVmomi/VmomiSupport.py", line 598, in <lambda>
    self.f(*(self.args + (obj,) + args), **kwargs)
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVmomi/VmomiSupport.py", line 388, in _InvokeMethod
    return self._stub.InvokeMethod(self, info, args)
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVmomi/SoapAdapter.py", line 1533, in InvokeMethod
    conn.request('POST', self.path, req, headers)
  File "/usr/lib/python3.10/http/client.py", line 1282, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1328, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1277, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1037, in _send_output
    self.send(msg)
  File "/usr/lib/python3.10/http/client.py", line 975, in send
    self.connect()
  File "/usr/lib/python3.10/http/client.py", line 1447, in connect
    super().connect()
  File "/usr/lib/python3.10/http/client.py", line 951, in connect
    self._tunnel()
  File "/usr/lib/python3.10/http/client.py", line 924, in _tunnel
    raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
OSError: Tunnel connection failed: 407 Proxy Authentication Required

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/connect_to_vc_via_proxy.py", line 24, in <module>
    si = SmartConnect(protocol='https',
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVim/connect.py", line 979, in SmartConnect
    return Connect(host=host,
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVim/connect.py", line 318, in Connect
    si, stub = __Login(host,
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVim/connect.py", line 503, in __Login
    reraise(vim.fault.HostConnectFault, fault, traceback)
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/six.py", line 718, in reraise
    raise value.with_traceback(tb)
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVim/connect.py", line 491, in __Login
    content = si.RetrieveContent()
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVmomi/VmomiSupport.py", line 598, in <lambda>
    self.f(*(self.args + (obj,) + args), **kwargs)
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVmomi/VmomiSupport.py", line 388, in _InvokeMethod
    return self._stub.InvokeMethod(self, info, args)
  File "/etc/vcp/applianceRTVEnv/lib/python3.10/site-packages/pyVmomi/SoapAdapter.py", line 1533, in InvokeMethod
    conn.request('POST', self.path, req, headers)
  File "/usr/lib/python3.10/http/client.py", line 1282, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1328, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1277, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/usr/lib/python3.10/http/client.py", line 1037, in _send_output
    self.send(msg)
  File "/usr/lib/python3.10/http/client.py", line 975, in send
    self.connect()
  File "/usr/lib/python3.10/http/client.py", line 1447, in connect
    super().connect()
  File "/usr/lib/python3.10/http/client.py", line 951, in connect
    self._tunnel()
  File "/usr/lib/python3.10/http/client.py", line 924, in _tunnel
    raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
pyVmomi.VmomiSupport.vim.fault.HostConnectFault: (vim.fault.HostConnectFault) {
   dynamicType = <unset>,
   dynamicProperty = (vmodl.DynamicProperty) [],
   msg = 'Tunnel connection failed: 407 Proxy Authentication Required',
   faultCause = <unset>,
   faultMessage = (vmodl.LocalizableMessage) []
}

I see below messages in the proxy logs:

1689748074.915 12 10.24.131.174 TCP_TUNNEL/200 3196 CONNECT 10.24.129.1:443 Administrator@NARKUM.COM HIER_DIRECT/10.24.129.1 - 1689748074.918 2 10.24.131.174 TCP_DENIED/407 5978 CONNECT 10.24.129.1:443 - HIER_NONE/- text/html 1689748274.797 16 10.24.131.174 TCP_TUNNEL/200 3167 CONNECT 10.24.129.1:443 Administrator@NARKUM.COM HIER_DIRECT/10.24.129.1 - 1689748274.801 2 10.24.131.174 TCP_DENIED/407 5982 CONNECT 10.24.129.1:443 - HIER_NONE/- text/html

pyVmomi connects to vCenter Server 2 times.

First time here: https://github.com/vmware/pyvmomi/blob/f0fe4e279cebdfdbca5bfce699063d15b1d3bd1d/pyVim/connect.py#L663

Second time here: https://github.com/vmware/pyvmomi/blob/f0fe4e279cebdfdbca5bfce699063d15b1d3bd1d/pyVmomi/SoapAdapter.py#L1533

It seems that first request is passing and second request is failing. I am not sure why that is happening.

Describe the solution you'd like

I would like to know what I am doing wrong and any WAR to solve it? Is is even possible to connect to vCenter server via kerberos authenticated proxy sever using pyVmomi? I am trying to do register/unregister a plugin on vCenter server.

Describe alternatives you've considered

No response

Additional context

No response

VedaNiks commented 11 months ago

@DanielDraganov Can you please let me know if adding support for communication with vCenter Server via kerberos authenticated proxy sever is possible? We need to plan release for our product which needs this functionality.

VedaNiks commented 11 months ago

I could debug this issue and found the root cause. pyVmomi connects to vCenter Server twice:

  1. In __FindSupportedVersion method, that returns the most preferred API version supported by the specified server
  2. In Connect method, that login and return the service instance object.

For each of this connection to pass through the kerberos authenticated proxy server, a new Proxy-Authorization header is needed.

We will need to modify SmartConnect method to allow multiple Proxy-Authorization headers. I have created a new method [since we'll need 2 custom headers]

def SmartConnectKerberosProxy(protocol='https',
                              host='localhost',
                              port=443,
                              user='root',
                              pwd='',
                              service="hostd",
                              path="/sdk",
                              preferredApiVersions=None,
                              keyFile=None,
                              certFile=None,
                              httpProxyHost=None,
                              httpProxyPort=80,
                              thumbprint=None,
                              sslContext=None,
                              httpConnectionTimeout=None,
                              connectionPoolTimeout=CONNECTION_POOL_IDLE_TIMEOUT_SEC,
                              token=None,
                              tokenType=None,
                              disableSslCertValidation=False,
                              customHeaders=[],
                              # Deprecated
                              b64token=None,
                              # Deprecated
                              mechanism='userpass'):
    """
    Determine the most preferred API version supported by the specified server,
    then connect to the specified server using that API version, login and return
    the service instance object.

    Throws any exception back to caller. The service instance object is
    also saved in the library for easy access.

    Clients should modify the service parameter only when connecting to
    a VMOMI server other than hostd/vpxd. For both of the latter, the
    default value is fine.

    @param protocol: What protocol to use for the connection (e.g. https or http).
    @type  protocol: string
    @param host: Which host to connect to.
    @type  host: string
    @param port: Port
    @type  port: int
    @param user: User
    @type  user: string
    @param pwd: Password
    @type  pwd: string
    @param service: Service
    @type  service: string
    @param path: Path
    @type  path: string
    @param preferredApiVersions: Acceptable API version(s) (e.g. vim.version.version9)
                                If a list of versions is specified the versions should
                                be ordered from most to least preferred.  If None is
                                specified, the list of versions support by pyVmomi will
                                be used.
    @type  preferredApiVersions: string or string list
    @param keyFile: ssl key file path
    @type  keyFile: string
    @param certFile: ssl cert file path
    @type  certFile: string
    @param httpProxyHost The host name of the proxy server.
    @type  httpProxyHost: string
    @param httpProxyPort The proxy server port.
    @type  httpProxyPort: string
    @param thumbprint: host cert thumbprint
    @type  thumbprint: string
    @param sslContext: SSL Context describing the various SSL options. It is only
                        supported in Python 2.7.9 or higher.
    @type  sslContext: SSL.Context
    @param httpConnectionTimeout: Timeout in secs for http requests.
    @type  httpConnectionTimeout: int
    @param connectionPoolTimeout: Timeout in secs for idle connections to close, specify
                                    negative numbers for never closing the connections
    @type  connectionPoolTimeout: int
    @type  token: string
    @param token: Authentication and Authorization token to use for the connection.
                    The presence of this token overrides the user and pwd parameters.
    @type disableSslCertValidation: bool
    @param disableSslCertValidation: Creates an unverified SSL context when True.
    @type  customHeaders: array
    @param customHeaders: Array of dictionaries with custom HTTP headers.
    @param b64token: base64 encoded token
           *** Deprecated: Use token instead ***
    @type  b64token: string
    @param mechanism: authentication mechanism: userpass or sspi
           *** Deprecated: Use tokenType instead ***
    @type  mechanism: string
    """
    if len(customHeaders) < 2:
        raise Exception("At least 2 Proxy-authorization headers are needed"
                        " to connect via Kerberos authenticated proxy server.")

    if preferredApiVersions is None:
        preferredApiVersions = GetServiceVersions('vim25')

    sslContext = getSslContext(host, sslContext, disableSslCertValidation)

    supportedVersion = __FindSupportedVersion(protocol, host, port, path,
                                              preferredApiVersions, sslContext,
                                              httpProxyHost, httpProxyPort,
                                              customHeaders[0])
    if supportedVersion is None:
        raise Exception("{0}:{1} is down or is not a VIM server"
                        .format(host, port))

    portNumber = protocol == "http" and -int(port) or int(port)

    return Connect(host=host,
                   port=portNumber,
                   user=user,
                   pwd=pwd,
                   service=service,
                   adapter='SOAP',
                   version=supportedVersion,
                   path=path,
                   keyFile=keyFile,
                   certFile=certFile,
                   httpProxyHost=httpProxyHost,
                   httpProxyPort=httpProxyPort,
                   thumbprint=thumbprint,
                   sslContext=sslContext,
                   httpConnectionTimeout=httpConnectionTimeout,
                   connectionPoolTimeout=connectionPoolTimeout,
                   token=token,
                   tokenType=tokenType,
                   disableSslCertValidation=disableSslCertValidation,
                   customHeaders=customHeaders[1],
                   b64token=b64token,
                   mechanism=mechanism)
DanielDraganov commented 1 week ago

Hello, With the latest major release of pyVmomi the connection logic was simplified and streamlined. However, this does not affect the Kerberos usage with the provided code sample. It's not clear if this specific use case requirement comes from Kerberos or from requests-kerberos but nevertheless this is not a pyVmomi related issue. Another workaround is to use Connect() instead of the SmartConnect() wrapper and provide a static version. It's enough for most of the use cases.