Skip to content

Commit c084f00

Browse files
authored
Merge pull request #40 from github/vdye/mtls
Add option to configure client certificate authentication
2 parents 4ba9ca2 + ad46b85 commit c084f00

File tree

9 files changed

+283
-11
lines changed

9 files changed

+283
-11
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ server, you can manage the web server process itself using these commands:
152152
Finally, if you want to run the web server process directly in your terminal,
153153
for debugging purposes, then you can run `git-bundle-web-server`.
154154

155+
### Additional resources
156+
157+
Detailed guides to more complex administration tasks or user workflows can be
158+
found in the [`docs/tutorials`](./docs/tutorials/) directory of this repository.
159+
155160
## Local development
156161

157162
### Building

cmd/git-bundle-server/web-server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (w *webServerCmd) startServer(ctx context.Context, args []string) error {
7777
parser.Visit(func(f *flag.Flag) {
7878
if webServerFlags.Lookup(f.Name) != nil {
7979
value := f.Value.String()
80-
if f.Name == "cert" || f.Name == "key" {
80+
if f.Name == "cert" || f.Name == "key" || f.Name == "client-ca" {
8181
// Need the absolute value of the path
8282
value, err = filepath.Abs(value)
8383
if err != nil {

cmd/git-bundle-web-server/bundle-server.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
44
"context"
5+
"crypto/tls"
6+
"crypto/x509"
57
"fmt"
68
"net/http"
79
"os"
@@ -28,8 +30,11 @@ type bundleWebServer struct {
2830
}
2931

3032
func NewBundleWebServer(logger log.TraceLogger,
31-
port string, certFile string, keyFile string,
32-
) *bundleWebServer {
33+
port string,
34+
certFile string, keyFile string,
35+
tlsMinVersion uint16,
36+
clientCAFile string,
37+
) (*bundleWebServer, error) {
3338
bundleServer := &bundleWebServer{
3439
logger: logger,
3540
serverWaitGroup: &sync.WaitGroup{},
@@ -43,13 +48,31 @@ func NewBundleWebServer(logger log.TraceLogger,
4348
Addr: ":" + port,
4449
}
4550

46-
if certFile != "" {
47-
bundleServer.listenAndServeFunc = func() error { return bundleServer.server.ListenAndServeTLS(certFile, keyFile) }
48-
} else {
51+
// No TLS configuration to be done, return
52+
if certFile == "" {
4953
bundleServer.listenAndServeFunc = func() error { return bundleServer.server.ListenAndServe() }
54+
return bundleServer, nil
55+
}
56+
57+
// Configure for TLS
58+
tlsConfig := &tls.Config{
59+
MinVersion: tlsMinVersion,
60+
}
61+
bundleServer.server.TLSConfig = tlsConfig
62+
bundleServer.listenAndServeFunc = func() error { return bundleServer.server.ListenAndServeTLS(certFile, keyFile) }
63+
64+
if clientCAFile != "" {
65+
caBytes, err := os.ReadFile(clientCAFile)
66+
if err != nil {
67+
return nil, err
68+
}
69+
certPool := x509.NewCertPool()
70+
certPool.AppendCertsFromPEM(caBytes)
71+
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
72+
tlsConfig.ClientCAs = certPool
5073
}
5174

52-
return bundleServer
75+
return bundleServer, nil
5376
}
5477

5578
func (b *bundleWebServer) parseRoute(ctx context.Context, path string) (string, string, string, error) {

cmd/git-bundle-web-server/main.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ func main() {
2626
port := utils.GetFlagValue[string](parser, "port")
2727
cert := utils.GetFlagValue[string](parser, "cert")
2828
key := utils.GetFlagValue[string](parser, "key")
29+
tlsMinVersion := utils.GetFlagValue[uint16](parser, "tls-version")
30+
clientCA := utils.GetFlagValue[string](parser, "client-ca")
2931

3032
// Configure the server
31-
bundleServer := NewBundleWebServer(logger, port, cert, key)
33+
bundleServer, err := NewBundleWebServer(logger,
34+
port,
35+
cert, key,
36+
tlsMinVersion,
37+
clientCA,
38+
)
39+
if err != nil {
40+
logger.Fatal(ctx, err)
41+
}
3242

3343
// Start the server asynchronously
3444
bundleServer.StartServerAsync(ctx)

cmd/utils/common-args.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package utils
22

33
import (
44
"context"
5+
"crypto/tls"
56
"flag"
67
"fmt"
78
"strconv"
9+
"strings"
810
)
911

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

4042
// Sets of flags shared between multiple commands/programs
4143

44+
type tlsVersionValue uint16
45+
46+
var tlsVersions = map[tlsVersionValue]string{
47+
tls.VersionTLS11: "tlsv1.1",
48+
tls.VersionTLS12: "tlsv1.2",
49+
tls.VersionTLS13: "tlsv1.3",
50+
}
51+
52+
func (v *tlsVersionValue) String() string {
53+
if strVal, ok := tlsVersions[*v]; ok {
54+
return strVal
55+
} else {
56+
panic(fmt.Sprintf("invalid tlsVersionValue '%d'", *v))
57+
}
58+
}
59+
60+
func (v *tlsVersionValue) Set(strVal string) error {
61+
strLower := strings.ToLower(strVal)
62+
for val, str := range tlsVersions {
63+
if str == strLower {
64+
*v = val
65+
return nil
66+
}
67+
}
68+
69+
// add details to "invalid value for flag" message
70+
validTlsVersions := []string{}
71+
for _, str := range tlsVersions {
72+
validTlsVersions = append(validTlsVersions, "'"+str+"'")
73+
}
74+
return fmt.Errorf("valid TLS versions are: %s", strings.Join(validTlsVersions, ", "))
75+
}
76+
77+
func (v *tlsVersionValue) Get() any {
78+
return uint16(*v)
79+
}
80+
4281
func WebServerFlags(parser argParser) (*flag.FlagSet, func(context.Context)) {
4382
f := flag.NewFlagSet("", flag.ContinueOnError)
4483
port := f.String("port", "8080", "The port on which the server should be hosted")
4584
cert := f.String("cert", "", "The path to the X.509 SSL certificate file to use in securely hosting the server")
4685
key := f.String("key", "", "The path to the certificate's private key")
86+
tlsVersion := tlsVersionValue(tls.VersionTLS12)
87+
f.Var(&tlsVersion, "tls-version", "The minimum TLS version the server will accept")
88+
f.String("client-ca", "", "The path to the client authentication certificate authority PEM")
4789

4890
// Function to call for additional arg validation (may exit with 'Usage()')
4991
validationFunc := func(ctx context.Context) {

docs/man/git-bundle-server.adoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,12 @@ Generate an incremental bundle with latest updates to rust-lang/rust:
163163
$ git-bundle-server update rust-lang/rust
164164
----
165165

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

169169
[source,console]
170170
----
171-
$ git-bundle-server web-server start --force --port 443 --cert server.crt --key server.key
171+
$ git-bundle-server web-server start --force --port 443 --client-ca ca.pem --cert server.crt --key server.key
172172
----
173173

174174
== SEE ALSO

docs/man/server-options.asc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,22 @@
1010
*--key* _path_:::
1111
Use the contents of the specified file as the private key of the X.509 SSL
1212
certificate specified with *--cert*.
13+
14+
*--tls-version* _version_:::
15+
If the web server is configured for TLS (i.e., *--cert* and *--key* are
16+
specified), reject requests using a version of TLS less than the given
17+
version. The _version_ must be one of the following non-case sensitive values:
18+
19+
- tlsv1.1
20+
- tlsv1.2
21+
- tlsv1.3
22+
23+
+
24+
These strings match those used in the *http.sslVersion* Git config setting
25+
(see man:git-config[1]). The default value is *tlsv1.2*. If the server is not
26+
configured for TLS, this option is a no-op.
27+
28+
*--client-ca* _path_:::
29+
Require that requests to the bundle server include a client certificate that
30+
can be validated by the certificate authority file at the specified _path_.
31+
No-op if *--cert* and *--key* are not configured.

docs/tutorials/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Tutorials
2+
3+
This directory contains a series of in-depth tutorials regarding administration
4+
and usage of the bundle server.

docs/tutorials/mtls.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Configuring mTLS authentication for the web server
2+
3+
[Mutual TLS (mTLS)][mtls] is a mechanism for mutual authentication between a
4+
server and client. Configuring mTLS for a bundle server allows a server
5+
maintainer to limit bundle server access to only the users that have been
6+
provided with a valid certificate and establishes confidence that users are
7+
interacting with a valid bundle server.
8+
9+
[mtls]: https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/
10+
11+
## mTLS limitations
12+
13+
mTLS in the bundle server is configured **server-wide**, so it only provides
14+
only a limited layer of protection against unauthorized access. Importantly,
15+
**any** user with a valid client cert/private key pair will be able to access
16+
**any** content on the bundle server. The implications of this include:
17+
18+
- If the bundle server manages repositories with separately controlled access,
19+
providing a user with a valid client cert/key for the bundle server may
20+
accidentally grant read access to Git data the user is not authorized to
21+
access on the remote host.
22+
- If the remote host has branch-level security then the bundles may contain Git
23+
objects reachable from restricted branches.
24+
25+
## Creating certificates
26+
27+
mTLS connections involve the verification of two X.509 certificates: one from
28+
the server, the other from the client. These certificates may be "self-signed",
29+
or issued by a separate certificate authority; either will work with the bundle
30+
server.
31+
32+
For both the server and client(s), both a public certificate (`.pem` file) and a
33+
private key must be generated. Self-signed pairs can easily be generated with
34+
OpenSSL; for example:
35+
36+
```bash
37+
openssl req -x509 -newkey rsa:4096 -days 365 -keyout cert.key -out cert.pem
38+
```
39+
40+
The above command will prompt the user to create a password for the 4096-bit RSA
41+
private key stored in `cert.key` (for no password, use the `-nodes` option),
42+
then fill in certificate metadata including locality information, company, etc.
43+
The resulting certificate - stored in `cert.pem` - will be valid for 365 days.
44+
45+
> :rotating_light: If the "Common Name" of the server certificate does not match
46+
> the bundle web server hostname (e.g. `localhost`), connections to the web
47+
> server may fail.
48+
49+
If instead generating a certificate signed by a certificate authority (which can
50+
itself be a self-signed certificate), a private key and _certificate signing
51+
request_ must first be generated:
52+
53+
```bash
54+
openssl req -new -newkey rsa:4096 -days 365 -keyout cert.key -out cert.csr
55+
```
56+
57+
The user will be prompted to fill in the same certificate metadata (`-nodes` can
58+
again be used to skip the password requirement on the private key). Once the
59+
request is generated (in `cert.csr`), the request can be signed with the CA (in
60+
the following example, with public certificate `ca.pem` and private key
61+
`ca.key`):
62+
63+
```bash
64+
openssl x509 -req -in cert.csr -CA ca.pem -CAkey ca.key -out cert.pem
65+
```
66+
67+
This generates the CA-signed certificate `cert.pem`.
68+
69+
### :rotating_light: IMPORTANT: PROTECTING YOUR CREDENTIALS :rotating_light:
70+
71+
It is _extremely_ important that the private keys associated with the generated
72+
certificates are safeguarded against unauthorized access (e.g. in a password
73+
manager with minimal access).
74+
75+
If the server-side key is exposed, a malicious site could pose as a valid bundle
76+
server and mislead users into providing it credentials or other private
77+
information. Potentially-exposed server credentials should be replaced as soon
78+
as possible, with the appropriate certificate authority/self-signed cert (_not_
79+
the private key) distributed to users that use the server.
80+
81+
If a client-side key is exposed, an unauthorized user or malicious actor will
82+
gain access to the bundle server and all content contained within it. **The
83+
bundle server does not provide a mechanism for revoking certificates**, so
84+
credentials will need to be rolled depending on how client certificates were
85+
generated:
86+
87+
- If the `--client-ca` used by the bundle web server is a self-signed
88+
certificate corresponding to a single client, a new certificate/key pair will
89+
need to be generated and the bundle web server restarted[^1] to use the new
90+
`--client-ca` file.
91+
- If the `--client-ca` is a concatenation of self-signed client certificates,
92+
the compromised certificate will need to be removed from the file and the
93+
bundle web server restarted.
94+
- If the `--client-ca` is a certificate authority (a single certificate used to
95+
sign other certificates), the certificate authority and _all_ client
96+
certificates will need to be replaced.
97+
98+
## Configuring the web server
99+
100+
To configure the web server, three files are needed:
101+
102+
- If using self-signed client certificate(s), the client certificate `.pem`
103+
(which may contain one or multiple client certificates concatenated together)
104+
_or_ the certificate authority `.pem` used to sign client certificate(s). In
105+
the example below, this is `ca.pem`.
106+
- The server `.pem` certificate file. In the example below, this is
107+
`server.pem`.
108+
- The server private key file. In the example below, this is `server.key`.
109+
110+
The bundle server can then be configured with the `web-server` command to run in
111+
the background:
112+
113+
```bash
114+
git-bundle-server web-server start --force --port 443 --cert server.pem --key server.key --client-ca ca.pem
115+
```
116+
117+
Alternatively, the web server can be started directly:
118+
119+
```bash
120+
git-bundle-web-server --port 443 --cert server.pem --key server.key --client-ca ca.pem
121+
```
122+
123+
If the contents of any of the certificate or key files change, the web server
124+
process must be restarted. To reload the background web server daemon, run
125+
`git-bundle-server web-server stop` followed by `git-bundle-server web-server
126+
start`.
127+
128+
## Configuring Git
129+
130+
If cloning or fetching from the bundle server via Git, the client needs to be
131+
configured to both verify the server certificate and send the appropriate client
132+
certificate information. This configuration can be applied using environment
133+
variables or `.gitconfig` values. The required configuration is as follows:
134+
135+
| Config (Environment) | Value |
136+
| --- | --- |
137+
| [`http.sslVerify`][sslVerify] (`GIT_SSL_NO_VERIFY`) | `true` for config, `false` for environment var. |
138+
| [`http.sslCert`][sslCert] (`GIT_SSL_CERT`) | Path to the `client.pem` public cert file. |
139+
| [`http.sslKey`][sslKey] (`GIT_SSL_KEY`) | Path to the `client.key` private key file. |
140+
| [`http.sslCertPasswordProtected`][sslKeyPassword] (`GIT_SSL_CERT_PASSWORD_PROTECTED`) | `true` |
141+
| [`http.sslCAInfo`][sslCAInfo] (`GIT_SSL_CAINFO`) | Path to the certificate authority file, including the server self-signed cert _or_ CA.[^2] |
142+
| [`http.sslCAPath`][sslCAPath] (`GIT_SSL_CAPATH`) | Path to the directory containing certificate authority files, including the server self-signed cert _or_ CA.[^2] |
143+
144+
Configuring the certificate authority information, in particular, can be tricky.
145+
Git does not have separate `http` configurations for clones/fetches vs. bundle
146+
URIs; both will use the same settings. As a result, if cloning via HTTP(S) with
147+
a bundle URI, users will need to _add_ the custom bundle server CA to the system
148+
store. The process for adding to the system certificate authorities are
149+
platform-dependent; for example, Ubuntu uses the
150+
[`update-ca-certificates`][update-ca-certificates] command.
151+
152+
To avoid needing to add the bundle server CA to the trusted CA store, users can
153+
instead choose to clone via SSH. In that case, only the bundle URI will use the
154+
`http` settings, so `http.sslCAInfo` can point directly to the standalone server
155+
CA.
156+
157+
[sslVerify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify
158+
[sslCert]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCert
159+
[sslKey]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslKey
160+
[sslKeyPassword]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCertPasswordProtected
161+
[sslCAInfo]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCAInfo
162+
[sslCAPath]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCAPath
163+
[update-ca-certificates]: https://manpages.ubuntu.com/manpages/xenial/man8/update-ca-certificates.8.html
164+
165+
[^1]: If using the `git-bundle-server web-server` command _and_ using a
166+
different `--client-ca` path than the old certificate, the `--force` option
167+
must be used with `start` to refresh the daemon configuration.
168+
[^2]: These settings are passed to cURL internally, setting `CURLOPT_CAINFO` and
169+
`CURLOPT_CAPATH` respectively.

0 commit comments

Comments
 (0)