HTTPS Proxy

Introduction

In addition to securing your SSH accesses, by splitting the authentication part (ingress connection) and the authorization part (egress connection), The Bastion can do a similar job for HTTPS connections.

Note that there is an overhead (depending on your hardware setup) of several hundreds of milliseconds for each query-response trip, due to the fact that multiple processes are spawned for each query, to ensure proper security containment to the calling account's system user. It's probably a bad idea to use on a multi-million queries/day workload, or if each added millisecond to the query-response trip impacts the QoS of your service.

The primary use is for network devices, that happen to have more and more HTTPS APIs in addition to the usual conf terminal available through SSH. As the same commands are usually available from HTTPS and SSH on these devices, it would be too bad to secure the access to SSH through the bastion, but leave direct access to their HTTPS API!

Query workflow

The workflow is similar to the one used by SSH, e.g. two distinct connections (ingress and egress), with the egress connection using credentials stored on the bastion:

  • A client makes an HTTP request to the proxy, with the following information embedded in:

    • The type of request (GET or POST)

    • The complete URI, including the host of the remote HTTPS server it would like to send the request to

    • Potential body data for POST requests

    • Credentials to authenticate to the proxy on the ingress connection, namely the bastion account name and its proxy password (set by selfGenerateProxyPassword)

    • User name to use to authenticate on the remote HTTPS server (for the egress connection)

  • The bastion checks the provided credentials to authenticate the request against a known account (authentication part)

  • The bastion verifies whether the just-authenticated account has access rights to connect to the remote server as the specified remote user (authorization part)

  • The bastion uses the (group or personal) credentials stored on the bastion, to passthrough the HTTP request to the remote server, as the specified remote user

  • The bastion forwards the response to the client

Setting up the HTTPS Proxy

You should enable the HTTPS Proxy daemon, and configure it. Please check the osh-http-proxy.conf for more information.

Running a query through the proxy

First try

Once the proxy is running, we can try to query it:

curl https://bastion1.example.org:8443/
No authentication provided, and authentication is mandatory

Of course, the proxy only accepts to work when one is properly authenticated to it. To do this, one should have an account on the bastion, and use the selfGenerateProxyPassword command so that a new ingress password is set for their account. They'll then be able to authenticate to the proxy using the HTTP basic-auth method, and try to send a request to a remote server. To keep a high compatibility with HTTP clients and libraries that can be used on the ingress side, all the additional data required by the bastion to properly authenticate, authorize and passthrough the request is encoded in the user part of the widely supported HTTP Authorize header (basic-auth). The password part corresponds to the password we've generated just above.

The format of the user part is as follows:

BASTION_ACCOUNT@REMOTE_USER@REMOTE_HOST%REMOTE_PORT

The %REMOTE_PORT part is optional, and defaults to 443 if omitted. For example, to send a GET /info request to the remote network device named router12.example.org on the default port 443, using the remote account monitoring, through the bastion1.example.org bastion, having the HTTPS Proxy listening on its port 8443 and a bastion account robot-mon, one can use curl:

curl -u robot-mon@monitoring@router12.example.org https://bastion1.example.org:8443/info
Enter host password for user 'robot-mon@monitoring@router12.example.org':
This account doesn't have access to this user@host tuple (Access denied for robot-mon to monitoring@router12.example.org:443)

A password will be prompted: the password generated by selfGenerateProxyPassword should be entered. Remember: this is to authenticate yourself to the bastion (ingress connection), then the bastion will authenticate itself to the remote machine (egress connection), using credentials stored on the bastion, that your account must have access to.

In the above case, we entered the password correctly, but our account doesn't have access to the requested host monitoring@router12.example.org. This is what we need to do now.

Query headers

The following optional headers can be added by the client to its request, to modify the behavior of the proxy. They're only interpreted by the bastion, and are not forwarded to the remote device.

X-Bastion-Timeout

Timeout, in seconds, for the connection between the bastion and the remote device. Must be an integer, and defaults to the timeout option of the osh-http-proxy.conf.

X-Bastion-Egress-Protocol

Protocol to use for the connection between the bastion and the remote device, either https or http. Defaults to https. The protocol must be allowed by the allowed_egress_protocols option of the osh-http-proxy.conf.

X-Bastion-Allow-Downgrade

Set to 1 to allow the bastion to silently retry the request using TLSv1.0, if the remote device doesn't support any more recent TLS version. Defaults to 0.

X-Bastion-Enforce-Secure

Set to 1 to enforce the verification of the remote device's certificate, even if the insecure option of the osh-http-proxy.conf is enabled. Defaults to 0.

Only a few standard headers of the client request are forwarded to the remote device, namely Accept, Content-Type, Content-Length and Content-Encoding.

Headers sent to the remote device

The bastion adds the following headers to the request it sends to the remote device, which can be used for traceability purposes on the device side:

X-Bastion-Auth-Mode

How the egress credentials have been chosen, see the X-Bastion-Auth-Mode response header below.

X-Bastion-Ingress-Client-IP

IP of the client that sent the request to the bastion.

X-Bastion-Ingress-Client-Port

Source port of the client that sent the request to the bastion.

X-Bastion-Ingress-Client-User-Agent

User-Agent of the client that sent the request to the bastion.

X-Bastion-Ingress-Account

The bastion account of the client that sent the request.

X-Bastion-UniqID

The unique ID of the request on the bastion, to correlate with the bastion logs.

X-Bastion-Instance

The hostname of the bastion instance that handled the request.

Response headers

The bastion adds the following headers to its response to the client. Note that the headers sent by the remote device are not forwarded as-is to the client, except Content-Type and Content-Length. Some of the below headers are only present when relevant.

Server

The Bastion version that handled the request.

X-Bastion-Instance

The hostname of the bastion instance that handled the request.

X-Bastion-ReqID

The unique ID of the request on the bastion, to correlate with the bastion logs.

X-Bastion-Local-Status

Status of the request from the bastion standpoint. When the request has been forwarded to the remote device, this is 200 OK, and the HTTP status of the response is the one of the remote device (see X-Bastion-Remote-Status below). Otherwise, this is the error encountered by the bastion, for example 403 Access Denied or 504 Device Timeout, and the HTTP status of the response matches it.

X-Bastion-Remote-Host

The remote device host, as specified by the client.

X-Bastion-Remote-IP

The IP of the remote device, as resolved by the bastion.

X-Bastion-Auth-Mode

How the egress credentials have been chosen: self/explicit or group/explicit when the client specified which credentials to use, self/default or group/guessed when the bastion chose them from the accesses of the account, or legacy.

X-Bastion-Request-Length

Length, in bytes, of the body of the client request.

X-Bastion-Remote-Status

Only present when the request has been forwarded to the remote device (i.e. when X-Bastion-Local-Status is 200 OK): this is the HTTP status returned by the remote device, and the HTTP status of the response matches it. If the bastion couldn't get an answer from the remote device, for example because the connection or the TLS negotiation failed, this is a 500 error describing the issue, such as 500 Can't connect to 192.0.2.1:443.

X-Bastion-Remote-Server

The Server header returned by the remote device, if any.

X-Bastion-Remote-Location

When the remote device answered with a redirect that the bastion didn't follow, this is the absolute URL of the redirect target, as computed from the Location header returned by the device. The Location header itself is not forwarded, as it usually points directly to the remote device and not to the bastion. To follow this redirect, the client can send a new request to the bastion for this target, which will go through the usual access checks. Which redirects are followed by the bastion depends on the allow_redirects option of the osh-http-proxy.conf.

X-Bastion-Remote-Client-SSL-Version

TLS version used for the connection to the remote device, for example TLSv1_3, when using https as the egress protocol.

X-Bastion-Remote-Client-SSL-Cipher

TLS cipher used for the connection to the remote device, when using https as the egress protocol.

X-Bastion-Remote-Client-SSL-Cert-Subject

Subject of the certificate presented by the remote device, when using https as the egress protocol.

X-Bastion-Remote-Client-SSL-Cert-Issuer

Issuer of the certificate presented by the remote device, when using https as the egress protocol.

X-Bastion-Remote-Client-SSL-Warning

Warning about the TLS connection to the remote device, if any, for example Peer certificate not verified when the insecure option is enabled.

X-Bastion-Remote-Client-SSL-Cert-Verify

Only present when the insecure option of the osh-http-proxy.conf is enabled (and X-Bastion-Enforce-Secure is not set), when using https as the egress protocol. Even if the certificate of the remote device is not enforced in this case, the bastion still verifies it, exactly as it would have if insecure was disabled, and reports the result here: OK if the connection would have been accepted, or FAILED followed by the reason between parentheses, for example FAILED (self-signed certificate) or FAILED (certificate doesn't match myhost.example). This is a diagnostic information, to ease the migration of an infrastructure to insecure: false, by detecting beforehand which devices would then be refused, for example by monitoring this header, or by looking for it in the HTTP Proxy request logs. The same information is also logged to syslog, see TLS compliance logs below.

All the X-Bastion-Remote-Client-SSL-* headers are generated by the bastion itself: if the remote device sends such headers (including through HTML <meta http-equiv> tags), they're ignored. If the bastion followed redirects, they describe the TLS connection that returned the final answer.

X-Bastion-Egress-Timing

Time spent, in microseconds, on the request between the bastion and the remote device.

X-Bastion-Downgraded

Present and set to 1 when the bastion had to downgrade the connection to TLSv1.0, see X-Bastion-Allow-Downgrade above.

TLS compliance logs

The bastion logs to syslog, with the info level, one line of type http-proxy-tls per TLS connection made to a remote device, regardless of the insecure option of the osh-http-proxy.conf. It tells whether this connection has been accepted or refused because of the remote device's certificate. When insecure is enabled, the certificate is not enforced, so the line tells instead whether the connection would have been accepted if insecure was disabled (the same verification as for the X-Bastion-Remote-Client-SSL-Cert-Verify header described above). These lines also include the TLS version and cipher used, which can be used for compliance statistics. Note that there can be more than one such line per request, if the bastion had to downgrade the connection, or followed a redirect. Connections that failed for another reason than the certificate validity (e.g. connection refused, or no common TLS version or cipher) are not logged here.

Each line contains the following fields, in addition to the usual ones (such as uniqid, which can be used to find the corresponding request in the access logs):

  • account, auth_mode, user: the account that did the request, how the egress credentials were chosen, and the remote user

  • host, ip, port: the remote device, as specified by the client, its resolved IP, and its port. For a redirect followed by the bastion, these are the ones of the redirect target, so that each line describes the device whose certificate has been verified (with allow_redirects set to any, this can be another device than the one the client asked for)

  • enforced: 1 if the certificate validity was enforced (insecure disabled, or X-Bastion-Enforce-Secure set by the client), 0 otherwise

  • verdict: ok if the connection has been accepted (or, when enforced is 0, would have been accepted if insecure was disabled), failed otherwise

  • reason: why it has been (or would have been) refused, empty if verdict is ok

  • ssl_version, ssl_cipher: the TLS version and cipher used for the connection, empty if it has been refused

  • cert_subject, cert_issuer: the subject and issuer of the certificate presented by the remote device, empty if the connection has been refused

  • downgraded: 1 if the bastion had to downgrade the connection to TLSv1.0, 0 otherwise

For example, to list the remote devices that are (or would be, once insecure is disabled) refused:

grep 'http-proxy-tls' /var/log/bastion/bastion.log | grep 'verdict="failed"' | grep -Eo 'host="[^"]*" ip="[^"]*" port="[^"]*"' | sort | uniq -c

Access declaration

The access check is the same than the one done for SSH accesses, which means that oneself can have access to a remote host either through a personal access or a group access.

To get granted access to a remote device, through a personal access, either the selfAddPersonalAccess or the accountAddPersonalAccess shall be used (both are restricted commands) such as:

bssh --osh accountAddPersonalAccess --host router12.example.org --port 443 --user monitoring --force

Note the use of --force to skip the SSH connection test, which is useless in our case.

To use a group access instead, one of the aclkeepers of the group should use groupAddServer, such as:

bssh --osh groupAddServer --group netdevices --host router12.example.org --port 443 --user monitoring --force

Egress password

For personal accesses

If access to a remote device is granted to you through a personal access (using either the selfAddPersonalAccess or accountAddPersonalAccess commands), you must first generate a new set of credentials that will be stored on your bastion account, for egress connections. This is the equivalent of your personal egress keys for SSH, but in that case it's a password that will be used to authenticate using basic-auth to the remote server. You can generate this password using the selfGeneratePassword command:

bssh --osh selfGeneratePassword --do-it
*------------------------------------------------------------------------------*
|THIS IS A PRIVATE COMPUTER SYSTEM, UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED.|
|ALL CONNECTIONS ARE LOGGED. IF YOU ARE NOT AUTHORIZED, DISCONNECT NOW.        |
*------------------------------------------------------------------------------*
╭──bastion1.example.org───────────────────────────────the-bastion-3.03.99-rc1───
│ ▶ generating a new egress password for your account
├───────────────────────────────────────────────────────────────────────────────
│ Generated a new password of length 16 for your account, robot-mon, hashes follow:
│ md5crypt: $1$G0fo$2DH2OJQJ9bMgo5fUUuPeK.
│ sha256crypt: $5$2xd1aGuD$ze7px3olXdjWthSrdnzelm6avzT2kszx/voXms8/V00
│ sha512crypt: $6$udw2UNLs$tQ1p7ZYraOT4Woh1ZCGJNf.UAIh09nXPBf4ejpRurOY/fJUs6Dgh1WdkpY4pdCvKMQrPeetB42bNTSzIwJyGi1
│ This new password will now be used by default.
╰─────────────────────────────────────────────────────</selfGeneratePassword>───

As you can see, the password is stored on your bastion account, and is not printed: only its hashes are. With this information, the corresponding remote account can be provisioned on the device (usually, a network device). In our above example, an account named monitoring would have to be created on the remote device, using one of these hashes. Prefer to use the most secure hashing algorithm supported by the remote device.

To get your password (hash) list, you can use selfListPasswords:

bssh --osh selfListPasswords
*------------------------------------------------------------------------------*
|THIS IS A PRIVATE COMPUTER SYSTEM, UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED.|
|ALL CONNECTIONS ARE LOGGED. IF YOU ARE NOT AUTHORIZED, DISCONNECT NOW.        |
*------------------------------------------------------------------------------*
╭──bastion1.example.org───────────────────────────────the-bastion-3.03.99-rc1───
│ ▶ list your egress passwords
├───────────────────────────────────────────────────────────────────────────────
│ Current password created at Tue Jun 22 15:42:10 2021 by robot-mon
│ ... md5crypt: $1$G0fo$2DH2OJQJ9bMgo5fUUuPeK.
│ ... sha256crypt: $5$2xd1aGuD$ze7px3olXdjWthSrdnzelm6avzT2kszx/voXms8/V00
│ ... sha512crypt: $6$udw2UNLs$tQ1p7ZYraOT4Woh1ZCGJNf.UAIh09nXPBf4ejpRurOY/fJUs6Dgh1WdkpY4pdCvKMQrPeetB42bNTSzIwJyGi1
│
│ Fallback password 1 created at Wed Jun  2 08:00:01 2021 by robot-mon
│ ... md5crypt: $1$qF0M$2.rbRRGs66aPiEpc/SqGv/
│ ... sha256crypt: $5$E9qkC7D6$SG8BB.nXvwU0dB0Bq9S/sF5pDidLwSIDKCv95qNWhX0
│ ... sha512crypt: $6$druGNgSk$bzVHSvux/OOE2ZhDpabFekQU3GTsiKS7Yl/lLmb9gIAmjnFfR6gj7GzOniK2jdLtEcB/hQlhcx9TDgj5zHhVd.
│
╰────────────────────────────────────────────────────────</selfListPasswords>───

If the selfGeneratePassword command is used several times, the newly generated password will always override the previous one. Still, all the previous passwords are kept (archived) for good measure, and can be restored manually by a bastion admin. These passwords are named Fallback passwords in the output of selfListPasswords.

For group accesses

If the access to the remote device is given through a group, then the group's own credentials will be used. To this effect, one of the group owners should use the groupGeneratePassword command:

bssh --osh groupGeneratePassword --group netdevices --do-it
╭──bastion1.example.org───────────────────────────────the-bastion-3.03.99-rc1───
│ ▶ generating a new egress password for the group
├───────────────────────────────────────────────────────────────────────────────
│ Generated a new password of length 16 for group netdevices, hashes follow:
│ md5crypt: $1$9sb2$X8/pPBSLfQ0ddBGR/bzsT1
│ sha256crypt: $5$o6Jr8w0X$yQfLuX17tUwE1jfhhAX//vsn6KpXU5jUd7SCNbkYNH.
│ sha512crypt: $6$gyxMyjao$YNhZJPXZa4r838XKg2tfvvoV/Dtm5HKsyKt18BnvFfT.y.hZuSXRX9GhM4mA0hUsO9f0UBisO/WiK3vF/9qsL1
│ This new password will now be used by default.
╰────────────────────────────────────────────────────</groupGeneratePassword>───

As with the personal egress passwords, the password is stored on the bastion only, and is not printed: only its hashes are. With this information, the corresponding remote account can be provisioned on the device (usually, a network device). In our above example, an account named monitoring would have to be created on the remote device, using one of these hashes. Prefer to use the most secure hashing algorithm supported by the remote device.

To get the group's password (hash) list, one can use the groupListPasswords command:

bssh --osh groupListPasswords --group netdevices
*------------------------------------------------------------------------------*
|THIS IS A PRIVATE COMPUTER SYSTEM, UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED.|
|ALL CONNECTIONS ARE LOGGED. IF YOU ARE NOT AUTHORIZED, DISCONNECT NOW.        |
*------------------------------------------------------------------------------*
╭──bastion1.example.org───────────────────────────────the-bastion-3.03.99-rc1───
│ ▶ list the egress passwords of the group
├───────────────────────────────────────────────────────────────────────────────
│ Current password created at Tue Jun 29 10:21:38 2021 by slesimpl
│ ... md5crypt: $1$9sb2$X8/pPBSLfQ0ddBGR/bzsT1
│ ... sha256crypt: $5$o6Jr8w0X$yQfLuX17tUwE1jfhhAX//vsn6KpXU5jUd7SCNbkYNH.
│ ... sha512crypt: $6$gyxMyjao$YNhZJPXZa4r838XKg2tfvvoV/Dtm5HKsyKt18BnvFfT.y.hZuSXRX9GhM4mA0hUsO9f0UBisO/WiK3vF/9qsL1
╰───────────────────────────────────────────────────────</groupListPasswords>───

If the groupGeneratePassword command is used several times, the newly generated password will always override the previous one. Still, all the previous passwords are kept (archived) for good measure, and can be restored manually by a bastion admin. These passwords are named Fallback passwords in the output of groupListPasswords.