Skip to content

Add option to configure client certificate authentication #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ server, you can manage the web server process itself using these commands:
Finally, if you want to run the web server process directly in your terminal,
for debugging purposes, then you can run `git-bundle-web-server`.

### Additional resources

Detailed guides to more complex administration tasks or user workflows can be
found in the [`docs/tutorials`](./docs/tutorials/) directory of this repository.

## Local development

### Building
Expand Down
2 changes: 1 addition & 1 deletion cmd/git-bundle-server/web-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (w *webServerCmd) startServer(ctx context.Context, args []string) error {
parser.Visit(func(f *flag.Flag) {
if webServerFlags.Lookup(f.Name) != nil {
value := f.Value.String()
if f.Name == "cert" || f.Name == "key" {
if f.Name == "cert" || f.Name == "key" || f.Name == "client-ca" {
// Need the absolute value of the path
value, err = filepath.Abs(value)
if err != nil {
Expand Down
35 changes: 29 additions & 6 deletions cmd/git-bundle-web-server/bundle-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
Expand All @@ -28,8 +30,11 @@ type bundleWebServer struct {
}

func NewBundleWebServer(logger log.TraceLogger,
port string, certFile string, keyFile string,
) *bundleWebServer {
port string,
certFile string, keyFile string,
tlsMinVersion uint16,
clientCAFile string,
) (*bundleWebServer, error) {
bundleServer := &bundleWebServer{
logger: logger,
serverWaitGroup: &sync.WaitGroup{},
Expand All @@ -43,13 +48,31 @@ func NewBundleWebServer(logger log.TraceLogger,
Addr: ":" + port,
}

if certFile != "" {
bundleServer.listenAndServeFunc = func() error { return bundleServer.server.ListenAndServeTLS(certFile, keyFile) }
} else {
// No TLS configuration to be done, return
if certFile == "" {
bundleServer.listenAndServeFunc = func() error { return bundleServer.server.ListenAndServe() }
return bundleServer, nil
}

// Configure for TLS
tlsConfig := &tls.Config{
MinVersion: tlsMinVersion,
}
bundleServer.server.TLSConfig = tlsConfig
bundleServer.listenAndServeFunc = func() error { return bundleServer.server.ListenAndServeTLS(certFile, keyFile) }

if clientCAFile != "" {
caBytes, err := os.ReadFile(clientCAFile)
if err != nil {
return nil, err
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caBytes)
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
tlsConfig.ClientCAs = certPool
}

return bundleServer
return bundleServer, nil
}

func (b *bundleWebServer) parseRoute(ctx context.Context, path string) (string, string, string, error) {
Expand Down
12 changes: 11 additions & 1 deletion cmd/git-bundle-web-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ func main() {
port := utils.GetFlagValue[string](parser, "port")
cert := utils.GetFlagValue[string](parser, "cert")
key := utils.GetFlagValue[string](parser, "key")
tlsMinVersion := utils.GetFlagValue[uint16](parser, "tls-version")
clientCA := utils.GetFlagValue[string](parser, "client-ca")

// Configure the server
bundleServer := NewBundleWebServer(logger, port, cert, key)
bundleServer, err := NewBundleWebServer(logger,
port,
cert, key,
tlsMinVersion,
clientCA,
)
if err != nil {
logger.Fatal(ctx, err)
}

// Start the server asynchronously
bundleServer.StartServerAsync(ctx)
Expand Down
42 changes: 42 additions & 0 deletions cmd/utils/common-args.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package utils

import (
"context"
"crypto/tls"
"flag"
"fmt"
"strconv"
"strings"
)

// Helpers
Expand Down Expand Up @@ -39,11 +41,51 @@ func GetFlagValue[T any](parser argParser, name string) T {

// Sets of flags shared between multiple commands/programs

type tlsVersionValue uint16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to use tlsVersionValue in more places (where you currently use uint16?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tlsVersionValue isn't meant to be an externally-facing type, but rather an implementation of flag.Getter for use with the arg parser. Outside of the initial arg parsing, the value is only used as a uint16 and (because Go doesn't really have enums) it can't provide much in the way of compile-time or runtime checks on the correctness of a TLS version without making usage more cumbersome.


var tlsVersions = map[tlsVersionValue]string{
tls.VersionTLS11: "tlsv1.1",
tls.VersionTLS12: "tlsv1.2",
tls.VersionTLS13: "tlsv1.3",
}

func (v *tlsVersionValue) String() string {
if strVal, ok := tlsVersions[*v]; ok {
return strVal
} else {
panic(fmt.Sprintf("invalid tlsVersionValue '%d'", *v))
}
}

func (v *tlsVersionValue) Set(strVal string) error {
strLower := strings.ToLower(strVal)
for val, str := range tlsVersions {
if str == strLower {
*v = val
return nil
}
}

// add details to "invalid value for flag" message
validTlsVersions := []string{}
for _, str := range tlsVersions {
validTlsVersions = append(validTlsVersions, "'"+str+"'")
}
return fmt.Errorf("valid TLS versions are: %s", strings.Join(validTlsVersions, ", "))
}

func (v *tlsVersionValue) Get() any {
return uint16(*v)
}

func WebServerFlags(parser argParser) (*flag.FlagSet, func(context.Context)) {
f := flag.NewFlagSet("", flag.ContinueOnError)
port := f.String("port", "8080", "The port on which the server should be hosted")
cert := f.String("cert", "", "The path to the X.509 SSL certificate file to use in securely hosting the server")
key := f.String("key", "", "The path to the certificate's private key")
tlsVersion := tlsVersionValue(tls.VersionTLS12)
f.Var(&tlsVersion, "tls-version", "The minimum TLS version the server will accept")
f.String("client-ca", "", "The path to the client authentication certificate authority PEM")

// Function to call for additional arg validation (may exit with 'Usage()')
validationFunc := func(ctx context.Context) {
Expand Down
6 changes: 3 additions & 3 deletions docs/man/git-bundle-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ Generate an incremental bundle with latest updates to rust-lang/rust:
$ git-bundle-server update rust-lang/rust
----

Start an HTTPS web server on port 443 with certificate 'server.crt' and private
key 'server.key':
Start an HTTPS web server with mTLS authentication (given files 'ca.pem',
'server.crt', 'server.key'):

[source,console]
----
$ git-bundle-server web-server start --force --port 443 --cert server.crt --key server.key
$ git-bundle-server web-server start --force --port 443 --client-ca ca.pem --cert server.crt --key server.key
----

== SEE ALSO
Expand Down
19 changes: 19 additions & 0 deletions docs/man/server-options.asc
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,22 @@
*--key* _path_:::
Use the contents of the specified file as the private key of the X.509 SSL
certificate specified with *--cert*.

*--tls-version* _version_:::
If the web server is configured for TLS (i.e., *--cert* and *--key* are
specified), reject requests using a version of TLS less than the given
version. The _version_ must be one of the following non-case sensitive values:

- tlsv1.1
- tlsv1.2
- tlsv1.3

+
These strings match those used in the *http.sslVersion* Git config setting
(see man:git-config[1]). The default value is *tlsv1.2*. If the server is not
configured for TLS, this option is a no-op.

*--client-ca* _path_:::
Require that requests to the bundle server include a client certificate that
can be validated by the certificate authority file at the specified _path_.
No-op if *--cert* and *--key* are not configured.
4 changes: 4 additions & 0 deletions docs/tutorials/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Tutorials

This directory contains a series of in-depth tutorials regarding administration
and usage of the bundle server.
169 changes: 169 additions & 0 deletions docs/tutorials/mtls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Configuring mTLS authentication for the web server

[Mutual TLS (mTLS)][mtls] is a mechanism for mutual authentication between a
server and client. Configuring mTLS for a bundle server allows a server
maintainer to limit bundle server access to only the users that have been
provided with a valid certificate and establishes confidence that users are
interacting with a valid bundle server.

[mtls]: https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/

## mTLS limitations

mTLS in the bundle server is configured **server-wide**, so it only provides
only a limited layer of protection against unauthorized access. Importantly,
**any** user with a valid client cert/private key pair will be able to access
**any** content on the bundle server. The implications of this include:

- If the bundle server manages repositories with separately controlled access,
providing a user with a valid client cert/key for the bundle server may
accidentally grant read access to Git data the user is not authorized to
access on the remote host.
- If the remote host has branch-level security then the bundles may contain Git
objects reachable from restricted branches.

## Creating certificates

mTLS connections involve the verification of two X.509 certificates: one from
the server, the other from the client. These certificates may be "self-signed",
or issued by a separate certificate authority; either will work with the bundle
server.

For both the server and client(s), both a public certificate (`.pem` file) and a
private key must be generated. Self-signed pairs can easily be generated with
OpenSSL; for example:

```bash
openssl req -x509 -newkey rsa:4096 -days 365 -keyout cert.key -out cert.pem
```

The above command will prompt the user to create a password for the 4096-bit RSA
private key stored in `cert.key` (for no password, use the `-nodes` option),
then fill in certificate metadata including locality information, company, etc.
The resulting certificate - stored in `cert.pem` - will be valid for 365 days.

> :rotating_light: If the "Common Name" of the server certificate does not match
> the bundle web server hostname (e.g. `localhost`), connections to the web
> server may fail.

If instead generating a certificate signed by a certificate authority (which can
itself be a self-signed certificate), a private key and _certificate signing
request_ must first be generated:

```bash
openssl req -new -newkey rsa:4096 -days 365 -keyout cert.key -out cert.csr
```

The user will be prompted to fill in the same certificate metadata (`-nodes` can
again be used to skip the password requirement on the private key). Once the
request is generated (in `cert.csr`), the request can be signed with the CA (in
the following example, with public certificate `ca.pem` and private key
`ca.key`):

```bash
openssl x509 -req -in cert.csr -CA ca.pem -CAkey ca.key -out cert.pem
```

This generates the CA-signed certificate `cert.pem`.

### :rotating_light: IMPORTANT: PROTECTING YOUR CREDENTIALS :rotating_light:

It is _extremely_ important that the private keys associated with the generated
certificates are safeguarded against unauthorized access (e.g. in a password
manager with minimal access).

If the server-side key is exposed, a malicious site could pose as a valid bundle
server and mislead users into providing it credentials or other private
information. Potentially-exposed server credentials should be replaced as soon
as possible, with the appropriate certificate authority/self-signed cert (_not_
the private key) distributed to users that use the server.

If a client-side key is exposed, an unauthorized user or malicious actor will
gain access to the bundle server and all content contained within it. **The
bundle server does not provide a mechanism for revoking certificates**, so
credentials will need to be rolled depending on how client certificates were
generated:

- If the `--client-ca` used by the bundle web server is a self-signed
certificate corresponding to a single client, a new certificate/key pair will
need to be generated and the bundle web server restarted[^1] to use the new
`--client-ca` file.
- If the `--client-ca` is a concatenation of self-signed client certificates,
the compromised certificate will need to be removed from the file and the
bundle web server restarted.
- If the `--client-ca` is a certificate authority (a single certificate used to
sign other certificates), the certificate authority and _all_ client
certificates will need to be replaced.

## Configuring the web server

To configure the web server, three files are needed:

- If using self-signed client certificate(s), the client certificate `.pem`
(which may contain one or multiple client certificates concatenated together)
_or_ the certificate authority `.pem` used to sign client certificate(s). In
the example below, this is `ca.pem`.
- The server `.pem` certificate file. In the example below, this is
`server.pem`.
- The server private key file. In the example below, this is `server.key`.

The bundle server can then be configured with the `web-server` command to run in
the background:

```bash
git-bundle-server web-server start --force --port 443 --cert server.pem --key server.key --client-ca ca.pem
```

Alternatively, the web server can be started directly:

```bash
git-bundle-web-server --port 443 --cert server.pem --key server.key --client-ca ca.pem
```

If the contents of any of the certificate or key files change, the web server
process must be restarted. To reload the background web server daemon, run
`git-bundle-server web-server stop` followed by `git-bundle-server web-server
start`.

## Configuring Git

If cloning or fetching from the bundle server via Git, the client needs to be
configured to both verify the server certificate and send the appropriate client
certificate information. This configuration can be applied using environment
variables or `.gitconfig` values. The required configuration is as follows:

| Config (Environment) | Value |
| --- | --- |
| [`http.sslVerify`][sslVerify] (`GIT_SSL_NO_VERIFY`) | `true` for config, `false` for environment var. |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh consistency 😭

| [`http.sslCert`][sslCert] (`GIT_SSL_CERT`) | Path to the `client.pem` public cert file. |
| [`http.sslKey`][sslKey] (`GIT_SSL_KEY`) | Path to the `client.key` private key file. |
| [`http.sslCertPasswordProtected`][sslKeyPassword] (`GIT_SSL_CERT_PASSWORD_PROTECTED`) | `true` |
| [`http.sslCAInfo`][sslCAInfo] (`GIT_SSL_CAINFO`) | Path to the certificate authority file, including the server self-signed cert _or_ CA.[^2] |
| [`http.sslCAPath`][sslCAPath] (`GIT_SSL_CAPATH`) | Path to the directory containing certificate authority files, including the server self-signed cert _or_ CA.[^2] |

Configuring the certificate authority information, in particular, can be tricky.
Git does not have separate `http` configurations for clones/fetches vs. bundle
URIs; both will use the same settings. As a result, if cloning via HTTP(S) with
a bundle URI, users will need to _add_ the custom bundle server CA to the system
store. The process for adding to the system certificate authorities are
platform-dependent; for example, Ubuntu uses the
[`update-ca-certificates`][update-ca-certificates] command.

To avoid needing to add the bundle server CA to the trusted CA store, users can
instead choose to clone via SSH. In that case, only the bundle URI will use the
`http` settings, so `http.sslCAInfo` can point directly to the standalone server
CA.

[sslVerify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify
[sslCert]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCert
[sslKey]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslKey
[sslKeyPassword]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCertPasswordProtected
[sslCAInfo]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCAInfo
[sslCAPath]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCAPath
[update-ca-certificates]: https://manpages.ubuntu.com/manpages/xenial/man8/update-ca-certificates.8.html

[^1]: If using the `git-bundle-server web-server` command _and_ using a
different `--client-ca` path than the old certificate, the `--force` option
must be used with `start` to refresh the daemon configuration.
[^2]: These settings are passed to cURL internally, setting `CURLOPT_CAINFO` and
`CURLOPT_CAPATH` respectively.