Michael Gaffney 99a8777866 fix(db): Add index to session connection table
This index improves the performance for two queries on the
`session_connection` table. The execution time for the first query went
from 40.105 ms to 1.405 ms. The execution time for the second query went
from 13.808 ms to 0.217 ms.

Query 1 explain plan before index:

    Update on session_connection  (cost=6465.17..6557.68 rows=11 width=50) (actual time=38.868..39.147 rows=3 loops=1)
      ->  Nested Loop  (cost=6465.17..6557.68 rows=11 width=50) (actual time=38.471..38.506 rows=3 loops=1)
            ->  HashAggregate  (cost=6464.75..6464.86 rows=11 width=40) (actual time=38.447..38.450 rows=3 loops=1)
                  Group Key: (session_connection_1.public_id)::text
                  Batches: 1  Memory Usage: 24kB
                  ->  Nested Loop Semi Join  (cost=0.42..6464.72 rows=11 width=40) (actual time=29.184..38.434 rows=3 loops=1)
                        ->  Seq Scan on session_connection session_connection_1  (cost=0.00..6329.38 rows=16 width=20) (actual time=15.744..38.014 rows=23 loops=1)
                              Filter: ((worker_id)::text = 'w__________k'::text)
                              Rows Removed by Filter: 163607
                        ->  Index Scan using session_connection_state_connection_id_end_time_key on session_connection_state  (cost=0.42..8.45 rows=1 width=20) (actual time=0.016..0.016 rows=0 loops=23)
                              Index Cond: (((connection_id)::text = (session_connection_1.public_id)::text) AND (end_time IS NULL))
                              Filter: ((state = ANY ('{authorized,connected}'::text[])) AND (start_time < (CURRENT_TIMESTAMP - '00:00:05'::interval)))
                              Rows Removed by Filter: 1
            ->  Index Scan using session_connection_pkey on session_connection  (cost=0.42..8.44 rows=1 width=20) (actual time=0.016..0.016 rows=1 loops=3)
                  Index Cond: ((public_id)::text = (session_connection_1.public_id)::text)
    Planning Time: 0.793 ms
    Trigger immutable_columns: time=0.474 calls=3
    Trigger update_connection_state_on_closed_reason: time=0.159 calls=3
    Trigger update_time_column: time=0.034 calls=3
    Trigger update_version_column: time=0.041 calls=3
    Trigger wh_update_session_connection: time=0.688 calls=3
    Execution Time: 40.105 ms

Query 1 explain plan after index:

    Update on session_connection  (cost=152.71..219.87 rows=8 width=50) (actual time=0.497..0.683 rows=3 loops=1)
      ->  Nested Loop  (cost=152.71..219.87 rows=8 width=50) (actual time=0.266..0.292 rows=3 loops=1)
            ->  HashAggregate  (cost=152.29..152.37 rows=8 width=40) (actual time=0.256..0.258 rows=3 loops=1)
                  Group Key: (session_connection_1.public_id)::text
                  Batches: 1  Memory Usage: 24kB
                  ->  Nested Loop Semi Join  (cost=4.94..152.27 rows=8 width=40) (actual time=0.199..0.251 rows=3 loops=1)
                        ->  Bitmap Heap Scan on session_connection session_connection_1  (cost=4.51..50.76 rows=12 width=20) (actual time=0.017..0.035 rows=10 loops=1)
                              Recheck Cond: ((worker_id)::text = 'w__________k'::text)
                              Heap Blocks: exact=9
                              ->  Bitmap Index Scan on session_connection_worker_id_closed_reason_ix  (cost=0.00..4.51 rows=12 width=0) (actual time=0.011..0.011 rows=10 loops=1)
                                    Index Cond: ((worker_id)::text = 'w__________k'::text)
                        ->  Index Scan using session_connection_state_connection_id_end_time_key on session_connection_state  (cost=0.42..8.45 rows=1 width=20) (actual time=0.021..0.021 rows=0 loops=10)
                              Index Cond: (((connection_id)::text = (session_connection_1.public_id)::text) AND (end_time IS NULL))
                              Filter: ((state = ANY ('{authorized,connected}'::text[])) AND (start_time < (CURRENT_TIMESTAMP - '00:00:05'::interval)))
                              Rows Removed by Filter: 1
            ->  Index Scan using session_connection_pkey on session_connection  (cost=0.42..8.44 rows=1 width=20) (actual time=0.010..0.010 rows=1 loops=3)
                  Index Cond: ((public_id)::text = (session_connection_1.public_id)::text)
    Planning Time: 0.614 ms
    Trigger immutable_columns: time=0.279 calls=3
    Trigger update_connection_state_on_closed_reason: time=0.185 calls=3
    Trigger update_time_column: time=0.027 calls=3
    Trigger update_version_column: time=0.032 calls=3
    Trigger wh_update_session_connection: time=0.450 calls=3
    Execution Time: 1.405 ms

Query 2 explain plan before index:

    Limit  (cost=1002.40..6147.94 rows=1 width=253) (actual time=12.244..13.726 rows=1 loops=1)
      ->  Nested Loop Left Join  (cost=1002.40..6147.93 rows=1 width=253) (actual time=12.243..13.724 rows=1 loops=1)
            Join Filter: ((w.public_id)::text = (session_connection.worker_id)::text)
            CTE worker_config_tags
              ->  GroupAggregate  (cost=1.71..2.26 rows=22 width=59) (actual time=0.044..0.082 rows=22 loops=1)
                    Group Key: ct_1.worker_id, ct_1.source
                    ->  Sort  (cost=1.71..1.77 rows=22 width=49) (actual time=0.031..0.034 rows=22 loops=1)
                          Sort Key: ct_1.worker_id, ct_1.source
                          Sort Method: quicksort  Memory: 27kB
                          ->  Seq Scan on server_worker_tag ct_1  (cost=0.00..1.22 rows=22 width=49) (actual time=0.007..0.010 rows=22 loops=1)
            ->  Nested Loop Left Join  (cost=0.14..9.28 rows=1 width=245) (actual time=0.106..0.108 rows=1 loops=1)
                  Join Filter: ((w.public_id)::text = (ct.worker_id)::text)
                  ->  Nested Loop Left Join  (cost=0.14..8.72 rows=1 width=213) (actual time=0.103..0.104 rows=1 loops=1)
                        Join Filter: ((w.public_id)::text = (wt.worker_id)::text)
                        ->  Index Scan using server_worker_pkey on server_worker w  (cost=0.14..8.15 rows=1 width=181) (actual time=0.012..0.012 rows=1 loops=1)
                              Index Cond: ((public_id)::text = 'w__________k'::text)
                        ->  CTE Scan on worker_config_tags wt  (cost=0.00..0.55 rows=1 width=64) (actual time=0.090..0.090 rows=0 loops=1)
                              Filter: (((worker_id)::text = 'w__________k'::text) AND (source = 'api'::text))
                              Rows Removed by Filter: 22
                  ->  CTE Scan on worker_config_tags ct  (cost=0.00..0.55 rows=1 width=64) (actual time=0.001..0.002 rows=1 loops=1)
                        Filter: (((worker_id)::text = 'w__________k'::text) AND (source = 'configuration'::text))
                        Rows Removed by Filter: 8
            ->  GroupAggregate  (cost=1000.00..6136.36 rows=1 width=21) (actual time=12.133..13.610 rows=0 loops=1)
                  Group Key: session_connection.worker_id
                  ->  Gather  (cost=1000.00..6136.35 rows=1 width=13) (actual time=12.132..13.608 rows=0 loops=1)
                        Workers Planned: 2
                        Workers Launched: 2
                        ->  Parallel Seq Scan on session_connection  (cost=0.00..5136.25 rows=1 width=13) (actual time=9.012..9.012 rows=0 loops=3)
                              Filter: ((closed_reason IS NULL) AND ((worker_id)::text = 'w__________k'::text))
                              Rows Removed by Filter: 54544
    Planning Time: 0.398 ms
    Execution Time: 13.808 ms

Query 2 explain plan after index:

    Limit  (cost=2.82..20.03 rows=1 width=253) (actual time=0.132..0.134 rows=1 loops=1)
      ->  Nested Loop Left Join  (cost=2.82..20.02 rows=1 width=253) (actual time=0.131..0.133 rows=1 loops=1)
            Join Filter: ((w.public_id)::text = (session_connection.worker_id)::text)
            CTE worker_config_tags
              ->  GroupAggregate  (cost=1.71..2.26 rows=22 width=59) (actual time=0.046..0.086 rows=22 loops=1)
                    Group Key: ct_1.worker_id, ct_1.source
                    ->  Sort  (cost=1.71..1.77 rows=22 width=49) (actual time=0.033..0.035 rows=22 loops=1)
                          Sort Key: ct_1.worker_id, ct_1.source
                          Sort Method: quicksort  Memory: 27kB
                          ->  Seq Scan on server_worker_tag ct_1  (cost=0.00..1.22 rows=22 width=49) (actual time=0.007..0.011 rows=22 loops=1)
            ->  Nested Loop Left Join  (cost=0.14..9.28 rows=1 width=245) (actual time=0.120..0.120 rows=1 loops=1)
                  Join Filter: ((w.public_id)::text = (ct.worker_id)::text)
                  ->  Nested Loop Left Join  (cost=0.14..8.72 rows=1 width=213) (actual time=0.117..0.118 rows=1 loops=1)
                        Join Filter: ((w.public_id)::text = (wt.worker_id)::text)
                        ->  Index Scan using server_worker_pkey on server_worker w  (cost=0.14..8.15 rows=1 width=181) (actual time=0.011..0.012 rows=1 loops=1)
                              Index Cond: ((public_id)::text = 'w__________k'::text)
                        ->  CTE Scan on worker_config_tags wt  (cost=0.00..0.55 rows=1 width=64) (actual time=0.104..0.105 rows=0 loops=1)
                              Filter: (((worker_id)::text = 'w__________k'::text) AND (source = 'api'::text))
                              Rows Removed by Filter: 22
                  ->  CTE Scan on worker_config_tags ct  (cost=0.00..0.55 rows=1 width=64) (actual time=0.002..0.002 rows=1 loops=1)
                        Filter: (((worker_id)::text = 'w__________k'::text) AND (source = 'configuration'::text))
                        Rows Removed by Filter: 8
            ->  GroupAggregate  (cost=0.42..8.45 rows=1 width=21) (actual time=0.010..0.010 rows=0 loops=1)
                  Group Key: session_connection.worker_id
                  ->  Index Only Scan using session_connection_worker_id_closed_reason_ix on session_connection  (cost=0.42..8.44 rows=1 width=13) (actual time=0.010..0.010 rows=0 loops=1)
                        Index Cond: ((worker_id = 'w__________k'::text) AND (closed_reason IS NULL))
                        Heap Fetches: 0
    Planning Time: 0.544 ms
    Execution Time: 0.217 ms

Closes #3281
2023-06-05 15:22:41 -04:00
2021-10-15 14:03:26 -04:00
2023-05-16 11:49:23 -06:00
2023-05-31 16:01:03 +00:00
2023-02-10 11:16:03 -05:00
2020-10-13 09:15:51 -07:00
2023-06-01 15:11:57 +00:00
2022-10-12 12:05:15 -04:00

Boundary


Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose by contacting us at security@hashicorp.com.


Boundary provides simple and secure access to hosts and services.

Traditional approaches like SSH bastion hosts or VPNs require distributing and managing credentials, configuring network controls like firewalls, and exposing the private network. Boundary provides a secure way to access hosts and critical systems without having to manage credentials or expose your network, and is entirely open source.

Boundary is designed to be straightforward to understand, highly scalable, and resilient. It can run in clouds, on-prem, secure enclaves and more, and does not require an agent to be installed on every end host.

Unlike firewalls, Boundary performs per-access authentication and authorization checks, allowing for much higher level mappings of users to services or hosts than at network layers. Although complementary to secrets managers (like HashiCorp's own Vault), Boundary fills a different niche, allowing the credential that is eventually used to be hidden entirely from the user.

Getting Started

Boundary consists of two server components: Controllers, which serve the API and coordinate session requests; and Workers, which perform the actual session handling. A normal Boundary installation will consist of one or more Controllers paired with one or more Workers. A single Boundary binary can act in either of these two modes.

Additionally, Boundary provides a client that provides access to request and connect to authorized sessions.

Boundary does not require software to be installed on the endpoint hosts and services.

Requirements

Boundary has two external dependencies: a SQL database, and one or more KMSes. Both are readily available from cloud vendors, but can be satisfied by on-premises technologies as well.

  • The database contains Boundary's configuration and session information and must be accessible by Controller nodes. Values that are secrets (such as credentials) are encrypted in the database. Currently, PostgreSQL is supported as a database and has been tested with Postgres 12 and above. Boundary uses only common extensions and both hosted and self-run instances are supported. In most instances all that is needed is a database endpoint and appropriate credentials.

  • Any cloud KMS or Vault's Transit Secrets Engine can be used to satisfy the KMS requirement. Currently, two keys within the KMS are required: one for authenticating other cluster components, which must be accessible by both Controllers and Workers; and one for encrypting secret values in the database, which need only be accessible to Controllers. These keys can be changed over time (so long as the original key remains available for any decryption needs), and key derivation is used extensively to avoid key sprawl of these high-value keys. If available, other keys can be used for other purposes, such as recovery functionality and encryption of sensitive values in Boundary's config file.

Boundary has a dev mode that can be used for testing. In this mode both a Controller and Worker are started with a single command, and they have the following properties:

  • The Controller will start a PostgreSQL Docker container to use as storage. This container will be shut down and removed (if possible) when the Controller is (gracefully) shut down.
  • The Controller will use an internal KMS with ephemeral keys

Trying out Boundary

Running Boundary in a more permanent context requires a few more steps, such as writing some simple configuration files to tell the nodes how to reach their database and KMS. The steps below, along with the extra information needed for permanent installations, are detailed in our Installation Guide.

Build and Start Boundary in Dev Mode

NOTE: Do not use the main branch except for dev or test cases. Boundary 0.10 introduced release branches which should be safe to track, however, migrations in main may be renumbered if needed. The Boundary team will not be able to provide assistance if running main over the long term results in migration breakages.

If you have the following requirements met locally:

  • Golang v1.19 or greater
  • Docker
  • Either the Boundary UI Dependencies for locally building the ui assets or gh cli for downloading pre-built ui assets.

You can get up and running with Boundary quickly. Simply run:

make install

This will build Boundary. (The first time this is run it will fetch and compile UI assets; which will take a few extra minutes.) Once complete, run Boundary in dev mode:

$GOPATH/bin/boundary dev

Please note that development may require other tools; to install the set of tools at the versions used by the Boundary team, run:

make tools

Without doing so, you may encounter errors while running make install. It is important to also note that using make tools will install various tools used for Boundary development to the normal Go binary directory; this may overwrite or take precedence over tools that might already be installed on the system.

Specify a UI Commitish at Build Time

By default the UI will be built from a preselected commit ID from the UI repo. A different commitish from which to build UI assets may be specified via the UI_COMMITISH environment variable. For example:

UI_COMMITISH=feature-branch make build-ui

will update your local UI assets.

UI Build Troubleshooting

UI assets are built within a Docker container. If you encounter issues with this build step, trying increasing memory and swap available to Docker.

UI Development

It would be impractical to rebuild the binary on every change when actively developing the UI. To make UI development more convenient, the binary supports a passthrough directory. This is an arbitrary local directory from which UI assets are served. Note this option is only available in dev mode. For example:

BOUNDARY_DEV_UI_PASSTHROUGH_DIR=/boundary-ui/ui/admin/dist ~/go/bin/boundary dev

Download and Run from Release Page

Download the latest release of the server binary and appropriate desktop client(s) from our releases page

Start Boundary

Start the server binary with:

boundary dev

This will start a Controller service listening on http://127.0.0.1:9200 for incoming API requests and a Worker service listening on http://127.0.0.1:9202 for incoming session requests. It will also create various default resources and display various useful pieces of information, such as a login name and password that can be used to authenticate.

Configuring Resources

For a simple test of Boundary in dev mode you don't generally need to configure any resources at all! But it's useful to understand what dev mode did for you so you can then take further steps. By default, dev mode will create:

  • The global Scope for initial authentication, containing a Password-type Auth Method, along with an Account for login.
  • An organization Scope under global, and a project Scope inside the organization.
  • A Host Catalog with a default Host Set, which itself contains a Host with the address of the local machine (127.0.0.1)
  • A Target mapping the Host Set to a set of connection parameters, with a default port of 22 (e.g. SSH)

You can of course go into Boundary's web UI or use its API to change these default values, for instance if you want to connect to a different host or need to modify the port on which to to connect.

Making the Connection

Next, let's actually make a connection to your local SSH daemon via Boundary:

  1. Authenticate to Boundary; using default dev values, this would be boundary authenticate password -auth-method-id ampw_1234567890 -login-name admin -password password. (Note that if you do not include the password flag you will be prompted for it.)
  2. Run boundary connect ssh -target-id ttcp_1234567890. If you want to adjust the username, pass -username <name> to the command.

A lot more is possible with Boundary, even at this early stage. Check out the possibilities for target configuration to test out limiting (or increasing) the number of connections per session or setting a maximum time limit; try canceling an active session from the sessions page or via boundary sessions, make your own commands with boundary connect -exec, and so on.

Going Further

This example is a simple way to get started but omits several key steps that could be taken in a production context:

  • Using a firewall or other means to restrict the set of hosts allowed to connect to a local service to only Boundary Worker nodes, thereby making Boundary the only means of ingress to a host
  • Using the Boundary Terraform provider to easily integrate Boundary with your existing code-based infrastructure
  • Pointing a BI tool (PowerBI, Tableau, etc.) at Boundary's session warehouse to generate insights and look for anomalies with respect to session access

There are many, many more things that Boundary will do in the future in terms of integrations, features, and more. We have a long roadmap planned out, so stay tuned for information about new features and capabilities!

Contributing

Thank you for your interest in contributing! Please refer to CONTRIBUTING.md for guidance.

S
Description
Community-maintained fork of HashiCorp Boundary Community Edition (BSL 1.1). Not affiliated with HashiCorp or IBM.
Readme
235 MiB
0 Stars 1 Watchers 0 Forks
Languages
Go 90.2%
PLpgSQL 7%
HCL 1.8%
Shell 0.7%
MDX 0.1%