No description
  • Go 93.7%
  • HTML 3.4%
  • JavaScript 2.4%
  • CSS 0.4%
  • Ruby 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-06 23:23:04 -04:00
app/concourse
cmd/concourse
internal/concourse chore(ccssh): add debug log for successful remote shell start 2026-09-24 10:02:43 -04:00
media
schemas
sql
sqlc
.gitignore
.golangci.yml
.goreleaser.yaml
.markdownlint.yaml
.svu.yml
Brewfile
CHANGELOG.md chore: update CHANGELOG.md for v0.3.2 release 2026-09-23 10:34:05 -04:00
commitlint.config.mjs
CONFIG.md docs(config): improve full example config (loopback_only: false, more realistic bind_address) 2026-09-23 00:31:20 -04:00
CREDENTIALS.md
eslint.config.js
go.mod build: bump qwee + other dependencies to latest versions 2026-10-06 23:23:04 -04:00
go.sum build: bump qwee + other dependencies to latest versions 2026-10-06 23:23:04 -04:00
LICENSE
package-lock.json build: bump qwee + other dependencies to latest versions 2026-10-06 23:23:04 -04:00
package.json build: bump qwee + other dependencies to latest versions 2026-10-06 23:23:04 -04:00
README.md docs(README): format DESTINATION PORT values as code in tables 2026-10-01 09:29:37 -04:00
sqlc.yaml
stave.yaml
stavefile.go chore(stavefile): correct typo in SecretsHook comment 2026-09-30 10:04:09 -04:00

concourse

screenshot of the banner printed by Concourse at the start of SSH sessions

Downloading

Just grab release for your specific architecture and OS from https://fergie.omer.land/omer/concourse/releases.

And you can of course also build from source!

Burning questions

What are the default/initial login credentials for the web UI???

Stop. Take a deep breath.

This is gonna be a bit more complicated than you're probably used to. But don't worry, it's not really that complicated.

concourse is a security tool. As such, it does not make sense to let whoever-happens-to-grab-the-reins-of-the-web-UI-first to be able to, through some "first use" flow, set the admin credentials & thus gain permanent access to / control of the system.

We want to make sure that the person who installed concourse is also the one who gets to set the admin credentials.

But how? Certainly, just putting the credentials in plaintext in some config file would not do. That's trouble. As is placing the credentials in plaintext in an environment variable, since environment variables are fairly easy to exfiltrate.

That's why, in concourse, you initialize the admin credentials this way.

Okay, so how do I configure this thing?

This way.

Awesome. How do I configure my TOTP?

Using the web API. (You can configure the bind-address like this, and the server also prints the URL to stdout when it's done starting up.)

Okay, enough with the rapid-fire questions. What does this thing even do?

What it does

Concourse is a sort of SSH-proxy meets SSH-bastion.

It is (heavily!) inspired by Warpgate, but it works a little differently:

  • Concourse runs on your computer AS a particular user.
  • You ssh into Concourse with a username like <username>%<eventual_target_machine>@<host_where_concourse_is_running>. (Though there are other options.)
  • There are then two layers of authentication:
    • You authenticate with Concourse -

      • Using the password you have configured
      • Or using a private key, whose public-key counterpart is in the authorized_keys of the user you're running Concourse as, on the machine where Concourse is running.
    • Concourse then proxies your SSH session to a session on <eventual_target_machine>.

      • If there is a private key, available to the user you're running Concourse as, on the machine where Concourse is running, that can be used to authenticate with <eventual_target_machine>, it will be used.
      • Otherwise, password (or other keyboard-interactive) authentication will be used.

Why you might want it

OKAY, WHAT DOES THIS GIVE ME, ABOVE & BEYOND REGULAR, DIRECT SSH...?

Two big things:

  • Rate-limiting.
  • Policy-driven TOTP, alongside password or private-public key authentication.

Rate-limiting

Concourse has highly configurable rate-limiters keyed on the following properties of SSH sessions:

  • IP that the original SSH request is coming from
  • IP of <eventual_target_machine>
  • Account on <eventual_target_machine>

As well as on the following properties of web sessions (used for creating & managing TOTP configs):

  • IP that the HTTP request is coming from
  • Account for web UI login

TOTPs (Temporary One-Time Passwords)

Concourse can add a TOTP layer above & beyond the password or private-public key authentication.

It supports:

  • Defining multiple "TOTP configs" (meaning, TOTPs with different secrets).
  • Configuring whether a TOTP will be required - and if so, which config it will be validated against - based on the IP or CIDR the SSH login is coming from.
    • The TOTP rules configuration evaluates IP/CIDR rules in order, which allows defining rules for entire subnets that will apply only if an earlier IP-specific rule was not matched. (See here for an example.)

TOTPs are configured through the web UI -

  • a QR is displayed when a new TOTP config is created (alongside a textual representation of the secret)
    • which you can use with an authenticator app to set up a generator for your new TOTP config

If you leave the "Name" field of the TOTP config empty, future logins to this account via the web API will also require a matching TOTP. (Though the empty-named TOTP config can also be used for second-factor SSH authentication, like any other, named TOTP config).

Note

By default, the Concourse web-server only binds to localhost:9923.

It is only intended for the kind of local setup discussed here, and use-cases for remote access to the web UI are rather unlikely.

For that reason, Concourse serves this web UI over plain HTTP.

If you absolutely need to serve this web UI non-locally, it is HIGHLY recommended to place it behind a reverse proxy (such as caddy) that does TLS termination - otherwise your security posture will be severely compromised.

For instructions on how to set the initial login credentials for the web UI - which will also be the credentials for the first hop of your SSH session, should a password be required – see CREDENTIALS.md.

SSH address binding, and whitelisting

In addition to the TOTP-specific rules, you can set the bind_address for the SSH server. (It defaults to localhost:2223, which is probably not of much use unless you set it to something broader.)

Furthermore, there is support for whitelisting. By default, the whitelist is empty, which means the whitelisting functionality is disabled. But if you populate it with one or more entries, only source IPs matching those entries (which can be IP addressed or CIDRs, with both IPv4 and IPv6 supported)will be allowed to log in to the SSH server. This is separate from the server binding to a particular address, and offers another line of defense.

A typical setup

With all of the above in mind, a typical way to use Concourse is as follows:

  • Restrict the standard sshd running on the machines on your LAN so that it only accepts connections from localhost and the LAN IP of the machine where Concourse is running.
    • Example:
AllowUsers *@localhost *@127.0.0.1 *@::1 192.168.1.42
  • Run Concourse with a broader bind_address (and whitelist, if applicable), and when away from your LAN, ssh to your machines through Concourse - which will provide both rate-limiting and TOTP protection.

Composite usernames & their interpretation

In the discussion above, an example of a composite username of the following form was given:

<username>%<eventual_target_machine>@<host_where_concourse_is_running>

The composite username - the portion prior to the @ - is considerably more flexible.

Delimiters

The following delimiters are supported (and can be mixed & matched as you please):

  • percentage sign (%)
  • hash mark (#)
  • colon (:)

Note

While all three delimiters work with the ssh CLI, at least some versions of the scp CLI require that you use % in particular.

Which is why the examples in this document all use %.

Pre-@ fields

None

SSH targets of the form <hostname> (with no @ nor, obviously any pre-@ fields) are interpreted as:

CONCOURSE USER: same as $USER on the source machine
CONCOURSE HOST: <hostname>
DESTINATION HOST: <hostname> (i.e., same host)
DESTINATION USER on DESTINATION HOST: same as $USER on the source machine
DESTINATION PORT on DESTINATION HOST: 22
One

SSH targets of the form foo@<hostname> are interpreted as:

CONCOURSE USER: foo
CONCOURSE HOST: <hostname>
DESTINATION HOST: <hostname> (i.e., same host)
DESTINATION USER on DESTINATION HOST: foo
DESTINATION PORT on DESTINATION HOST: 22
Two

SSH targets of the form foo%bar@<hostname> are interpreted as:

CONCOURSE USER: foo
CONCOURSE HOST: <hostname>
DESTINATION HOST: bar
DESTINATION USER on DESTINATION HOST: foo
DESTINATION PORT on DESTINATION HOST: 22
Three

SSH targets of the form foo%bar%baz@<hostname> are interpreted as:

CONCOURSE USER: baz
CONCOURSE HOST: <hostname>
DESTINATION HOST: bar
DESTINATION USER on DESTINATION HOST: foo
DESTINATION PORT on DESTINATION HOST: 22
Four

SSH targets of the form foo%bar%12345%baz@<hostname> are interpreted as:

CONCOURSE USER: baz
CONCOURSE HOST: <hostname>
DESTINATION HOST: bar
DESTINATION USER on DESTINATION HOST: foo
DESTINATION PORT on DESTINATION HOST: 12345

Building from source

Concourse uses stave (a fork of mage) as its build system.

To build this project from source:

  • Clone the repo.
  • Grab stave.
  • Run stave build to build Concourse from source.
  • Or run stave -l to see other things you can do.

License

This software is released under the Apache License 2.0.