blob: adebd924a248978a8a7a8086f39ed8233721685b [file] [log] [blame]
.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license
agreements. See the NOTICE file distributed with this work for additional information regarding
copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations
under the License.
.. include:: ../../../common.defs
.. highlight:: text
.. default-domain:: cpp
.. |Name| replace:: TLS Bridge
|Name|
**********
This plugin is used to provide TLS tunnels for connections between a Client and a Service via two
gateway |TS| instances using explicit proxying. By configuring the |TS| instances the level of
security in the tunnel can be easily controlled for all communications across the tunnels without
having to update the client or service.
Description
===========
The tunnel is sustained by two instances of |TS|.
.. uml::
:align: center
hide empty members
cloud "Cloud\nUntrusted\nNetworks" as Cloud
node "Ingress ATS"
node "Peer ATS"
[Client] <--> [Ingress ATS] : Unsecure
[Ingress ATS] <-> [Cloud] : Secure
[Cloud] <-> [Peer ATS] : Secure
[Peer ATS] <-u-> [Service] : Unsecure
[Ingress ATS] ..> [tls_bridge\nPlugin] : Uses
The ingress |TS| accepts an HTTP ``CONNECT`` request from the Client. This connection gets
intercepted by the |Name| plugin inside |TS| if the destination matches one of the configured
destinations. The plugin then makes a TLS connection to the peer |TS| using the configured level of
security. The original ``CONNECT`` request from the Client to the ingress |TS| is then sent to the
peer |TS| to create a connection from the peer |TS| to the Service. After this the Client has a
virtual circuit to the Service and can use any TCP based communication (including TLS). Effectively
the plugin causes the explicit proxy to work as if the Client had done the ``CONNECT`` directly to
the peer |TS|. Note this means the DNS lookup for the Service is done by the peer |TS|, not the
ingress |TS|.
The plugin is configured with a mapping of Service names to peer |TS| instances. The Service names
are URLs which will be in the original HTTP request made by the Client after connecting to the
ingress |TS|. This means the FQDN for the Service is resolved in the environment of the peer
|TS| and not the ingress |TS|.
Configuration
=============
|Name| requires at least two instances of |TS| (Ingress and Peer). The client connects to the
ingress |TS|, and the peer |TS| connects to the service. The Peer could in theory be configured to
connect on to a further |TS| instance, acting as the ingress to that peer, but that doesn't seem a
useful case.
#. Disable caching on |TS| in ``records.config``::
CONFIG proxy.config.http.cache.http INT 0
#. Configure the ports.
* The Peer |TS| must be listening on an SSL enabled proxy port. For instance, if the proxy port
for the Peer is 4443, then configuration in ``records.config`` would have::
CONFIG proxy.config.http.server_ports STRING 4443:ssl
* The Ingress |TS| must allow ``CONNECT`` to the Peer proxy port. This would be set in
``records.config`` by::
CONFIG proxy.config.http.connect_ports STRING 4443
The Ingress |TS| also needs ``proxy.config.http.server_ports`` configured to have proxy ports
to which the Client can connect.
#. By default |TS| requires remap in order to allow to outbound request to the peer. To disable this
requirement and allow all connections, use the setting ::
CONFIG proxy.config.url_remap.remap_required INT 0
In this case |TS| will act as an open proxy which is unlikely to be a good idea. Therefore if
this approach is used |TS| will need to run in a restricted environment or use access control
(via ``ip_allow.yaml`` or ``iptables``).
If this is unsuitable then an identity remap rule can be added for the peer |TS|. If the peer
|TS| was named "peer.ats" and it listens on port 4443, then the remap rule would be ::
map https://peer.ats:4443 https://peer.ats:4443
Remapping will be disabled for the user agent connection and so it will not need a rule.
#. If remap is required on the peer to enable the outbound connection from the peer to the service
(e.g. required remapping is not explicitly disabled) the destination port must be
explicitly stated [#]_. E.g. ::
map https://service:4443 https://service:4443
Note this remap rule cannot alter the actual HTTP transactions between the client and service
because those happen inside what is effectively a tunnel between the client and service,
supported by the two |TS| instances. This rule serves to allows the ``CONNECT`` sent from the
ingress to cause a tunnel connection from the peer to the service.
#. Configure the Ingress |TS| to verify the Peer server certificate::
CONFIG proxy.config.ssl.client.verify.server.policy STRING ENFORCED
#. Configure Certificate Authority used by the Ingress |TS| to verify the Peer server certificate.
If this is a directory, all of the certificates in the directory are treated as Certificate
Authorities. ::
CONFIG proxy.config.ssl.client.CA.cert.filename STRING </path/to/CA_certificate_file_name>
#. Configure the Ingress |TS| to provide a client certificate::
CONFIG proxy.config.ssl.client.cert.path STRING </path/to/certificate/dir>
CONFIG proxy.config.ssl.client.cert.filename STRING <server_certificate_file_name>
#. Configure the Peer |TS| to verify the Ingress client certificate::
CONFIG proxy.config.ssl.client.certification_level INT 2
#. Enable the |Name| plugin in ``plugin.config``. The plugin is configured by arguments in
``plugin.config``. These are arguments are in pairs of a *destination* and a *peer*. The
destination is an anchored regular expression which is matched against the host name in the Client
``CONNECT``. The destinations are checked in order and the first match is used to select the peer
|TS|. The peer should be an FQDN or IP address with an optional port. For the example above, if
the Peer |TS| was named "peer.ats" on port 4443 and the Service at ``*.service.com``, the
peer argument would be "peer.ats:4443". In ``plugin.config`` this would be::
tls_bridge.so .*[.]service[.]com peer.ats:4443
Note the '.' characters are escaped with brackets so that, for instance, "someservice.com" does
not match the rule.
If there was another service, "\*.altsvc.ats", via a different peer "altpeer.ats" on port 4443,
the configuration would be ::
tls_bridge.so .*[.]service[.]com peer.ats:4443 .*[.]altsvc.ats altpeer.ats:4443
Mappings can also be specified in an external file. For instance, if there was file named
"bridge.config" in the default |TS| configuration directory which contained mappings, the
``plugin.config`` configuration line could look like ::
tls_bridge.so .*[.]service[.]com peer.ats:4443 --file bridge.config
or
tls_bridge.so --file bridge.config .*[.]service[.]com peer.ats:4443
These are not identical - direct mappings and file mappings are processed in order. This means in
the first example, the direct mapping is checked before any mapping in "bridge.config", and in
the latter example the mappings in "bridge.config" are checked before the direct mappings. There
can be multiple "--file" arguments, which are processed in the order they appear in
"plugin.config". The file name can be absolute, or relative. If the file name is relative, it is
relative to the |TS| configuration directory. Therefore, in these examples, "bridge.config" must
be in the same directory as ``plugin.config``.
The contents of "bridge.config" must be one mapping per line, with a regular expression separated
by white space from the destination service. This is identical to the format in ``plugin.config``
except there is only one pair per line. E.g., valid content for "bridge.config" could be ::
# Primary service location.
.*[.]service[.]com peer.ats:4443
# Secondary.
.*[.]altsvc.ats altpeer.ats:4443
Leading whitespace on a line is ignored, and if the first non-whitespace character is '#' then
the entire line is ignored. Therefore if that is the content of "bridge.config", these two
lines in "plugin.config" would behave identically ::
tls_bridge.so --file bridge.config
tls_bridge.so .*[.]service[.]com peer.ats:4443 .*[.]altsvc.ats altpeer.ats:4443
Notes
=====
|Name| is distinct from more basic Layer 4 Routing available in |TS|. For the latter there is no
intercept or change of the TLS exchange between the Client and the Service. The exchange looks like
this
.. uml::
:align: center
actor Client
participant "Ingress TS" as Ingress
participant Service
Client <-[#green]> Ingress : //TCP Connect//
Client -[#blue]-> Ingress : <font color="blue">TLS: ""CLIENT HELLO""</font>
note over Ingress : Map SNI to upstream Service
Ingress <-[#green]> Service : //TCP Connect//
Ingress -[#blue]-> Service : <font color="blue">TLS: ""CLIENT HELLO""</font>
note right : Duplicate of data from Client.
note over Ingress : Forward bytes between Client <&arrow-thick-left> <&arrow-thick-right> Service
Client <--> Service
The key points are
* |TS| does no TLS negotiation at all. The properties of the connection between the Ingress |TS|
and the Service are completely determined by the Client and Server negotiation.
* No packets are modified, the ""CLIENT HELLO"" sent by the Ingress |TS| is an exact copy of that
sent to the Ingress |TS| by the Client. It is only examined for the SNI data in order to select
the Service.
Implementation
==============
The |Name| plugin uses :code:`TSHttpTxnIntercept` to gain control of the ingress Client session.
If the session is valid then a separate connection to the peer |TS| is created using
:code:`TSHttpConnect`.
After the ingress |TS| connects to the peer |TS| it sends a duplicate of the Client ``CONNECT``
request. This is processed by the peer |TS| to connect to the Service. After this both |TS|
instances then tunnel data between the Client and the Service, in effect becoming a transparent
tunnel.
The overall exchange looks like the following:
.. uml::
:align: center
@startuml
box "Client Network" #DDFFDD
actor Client
entity "User Agent\nVConn" as lvc
participant "Ingress ATS" as ingress
entity "Upstream\nVConn" as rvc
end box
box "Corporate Network" #DDDDFF
participant "Peer ATS" as peer
database Service
end box
Client -> ingress : TCP or TLS connect
activate lvc
Client -> ingress : HTTP CONNECT
ingress -> lvc : Intercept Transaction
ingress -> peer : TLS connect
activate rvc
note over ingress,peer : Secure Tunnel
ingress -> peer : HTTP CONNECT
note over peer : DNS for Service is\ndone here.
peer -> Service : TCP Connect
note over Client, Service : At this point data can flow between the Client and Server\nover the secure link as a virtual connection, including any TLS handshake.
Client <--> Service
lvc <-> ingress : <&arrow-thick-left> Move data <&arrow-thick-right>
ingress <-> rvc : <&arrow-thick-left> Move data <&arrow-thick-right>
note over ingress : Plugin explicitly moves this data.
@enduml
A detailed view of the plugin operation.
.. figure:: ../../../uml/images/TLS-Bridge-Plugin.svg
:align: center
A sequence diagram focusing on the request / response data flow. There is a :code:`NetVConn` for the
connection to the Peer |TS| which is omitted for clarity.
* Blue dotted lines are request or response data
* Green lines are network connections.
* Red lines are programmatic interactions.
* Black lines are hook call backs.
The :code:`200 OK` sent from the Peer |TS| is parsed and consumed by the plugin. An non-:code:`200` response
means there was an error and the tunnel is shut down. To deal with the Client response clean up the
response code is stored and used later during cleanup.
.. figure:: ../../../uml/images/TLS-Bridge-Messages.svg
:align: center
A restartable state machine is used to recognize the end of the Peer |TS| response. The initial part
of the response is easy because all that is needed is to wait until there is sufficient data for a
minimal parse. The end can be an arbitrary distance in to the stream and may not all be in the same
socket read.
.. uml::
:align: center
@startuml
[*] -r> State_0
State_0 --> State_1 : CR
State_1 --> State_0 : *
State_1 --> State_1 : CR
State_1 --> State_2 : LF
State_2 --> State_3 : CR
State_2 --> State_0 : *
State_3 -r> [*] : LF
State_3 --> State_1 : CR
State_3 --> State_0 : *
@enduml
Debugging
---------
Debugging messages for the plugin can be enabled with the "tls_bridge" debug tag.
.. rubric:: Footnotes
.. [#] This is likely due to a bug in |TS|, currently under investigation.