feat!: generate a self-signed certificate if no certificates are specified (#5973)

* feat: generate a self-signed certificate if no certificates are specified

Clouds like AWS automatically navigate to https://<ip-here>. This
allows us to bind to that immediately, serve a self-signed certificate,
then reroute to the access URL.

* Add new flag and deprecate old one

* Fix redirect if not using tunnel

* Add deprecation notice

* Fix TLS redirect

* Run `make gen`

* Fix bad test

* Fix gen
This commit is contained in:
Kyle Carberry 2023-02-02 11:08:35 -06:00 committed by GitHub
parent e27f7accd7
commit b9b402cd0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 132 additions and 38 deletions

View File

@ -32,6 +32,11 @@ func newConfig() *codersdk.DeploymentConfig {
Usage: "Specifies the wildcard hostname to use for workspace applications in the form \"*.example.com\".",
Flag: "wildcard-access-url",
},
RedirectToAccessURL: &codersdk.DeploymentConfigField[bool]{
Name: "Redirect to Access URL",
Usage: "Specifies whether to redirect requests that do not match the access URL host.",
Flag: "redirect-to-access-url",
},
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
Address: &codersdk.DeploymentConfigField[string]{
Name: "Address",
@ -300,11 +305,13 @@ func newConfig() *codersdk.DeploymentConfig {
Flag: "tls-address",
Default: "127.0.0.1:3443",
},
// DEPRECATED: Use RedirectToAccessURL instead.
RedirectHTTP: &codersdk.DeploymentConfigField[bool]{
Name: "Redirect HTTP to HTTPS",
Usage: "Whether HTTP requests will be redirected to the access URL (if it's a https URL and TLS is enabled). Requests to local IP addresses are never redirected regardless of this setting.",
Flag: "tls-redirect-http-to-https",
Default: true,
Hidden: true,
},
CertFiles: &codersdk.DeploymentConfigField[[]string]{
Name: "TLS Certificate Files",

View File

@ -4,6 +4,9 @@ package cli
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"database/sql"
@ -11,6 +14,7 @@ import (
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"net/http/pprof"
@ -267,6 +271,13 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.New("tls address must be set if tls is enabled")
}
// DEPRECATED: This redirect used to default to true.
// It made more sense to have the redirect be opt-in.
if os.Getenv("CODER_TLS_REDIRECT_HTTP") == "true" || cmd.Flags().Changed("tls-redirect-http-to-https") {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\n")
cfg.RedirectToAccessURL.Value = cfg.TLS.RedirectHTTP.Value
}
tlsConfig, err = configureTLS(
cfg.TLS.MinVersion.Value,
cfg.TLS.ClientAuth.Value,
@ -390,15 +401,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
cmd.Printf("%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURLParsed.String()), reason)
}
// Redirect from the HTTP listener to the access URL if:
// 1. The redirect flag is enabled.
// 2. HTTP listening is enabled (obviously).
// 3. TLS is enabled (otherwise they're likely using a reverse proxy
// which can do this instead).
// 4. The access URL has been set manually (not a tunnel).
// 5. The access URL is HTTPS.
shouldRedirectHTTPToAccessURL := cfg.TLS.RedirectHTTP.Value && cfg.HTTPAddress.Value != "" && cfg.TLS.Enable.Value && tunnel == nil && accessURLParsed.Scheme == "https"
// A newline is added before for visibility in terminal output.
cmd.Printf("\nView the Web UI: %s\n", accessURLParsed.String())
@ -769,8 +771,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
if shouldRedirectHTTPToAccessURL {
handler = redirectHTTPToAccessURL(handler, accessURLParsed)
if cfg.RedirectToAccessURL.Value {
handler = redirectToAccessURL(handler, accessURLParsed, tunnel != nil)
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
@ -1162,12 +1164,6 @@ func loadCertificates(tlsCertFiles, tlsKeyFiles []string) ([]tls.Certificate, er
if len(tlsCertFiles) != len(tlsKeyFiles) {
return nil, xerrors.New("--tls-cert-file and --tls-key-file must be used the same amount of times")
}
if len(tlsCertFiles) == 0 {
return nil, xerrors.New("--tls-cert-file is required when tls is enabled")
}
if len(tlsKeyFiles) == 0 {
return nil, xerrors.New("--tls-key-file is required when tls is enabled")
}
certs := make([]tls.Certificate, len(tlsCertFiles))
for i := range tlsCertFiles {
@ -1183,6 +1179,36 @@ func loadCertificates(tlsCertFiles, tlsKeyFiles []string) ([]tls.Certificate, er
return certs, nil
}
// generateSelfSignedCertificate creates an unsafe self-signed certificate
// at random that allows users to proceed with setup in the event they
// haven't configured any TLS certificates.
func generateSelfSignedCertificate() (*tls.Certificate, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 180),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, err
}
var cert tls.Certificate
cert.Certificate = append(cert.Certificate, derBytes)
cert.PrivateKey = privateKey
return &cert, nil
}
func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string) (*tls.Config, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
@ -1219,6 +1245,14 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles
if err != nil {
return nil, xerrors.Errorf("load certificates: %w", err)
}
if len(certs) == 0 {
selfSignedCertificate, err := generateSelfSignedCertificate()
if err != nil {
return nil, xerrors.Errorf("generate self signed certificate: %w", err)
}
certs = append(certs, *selfSignedCertificate)
}
tlsConfig.Certificates = certs
tlsConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
// If there's only one certificate, return it.
@ -1483,10 +1517,23 @@ func configureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile stri
return ctx, &http.Client{}, nil
}
func redirectHTTPToAccessURL(handler http.Handler, accessURL *url.URL) http.Handler {
// nolint:revive
func redirectToAccessURL(handler http.Handler, accessURL *url.URL, tunnel bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
redirect := func() {
http.Redirect(w, r, accessURL.String(), http.StatusTemporaryRedirect)
}
// Only do this if we aren't tunneling.
// If we are tunneling, we want to allow the request to go through
// because the tunnel doesn't proxy with TLS.
if !tunnel && accessURL.Scheme == "https" && r.TLS == nil {
redirect()
return
}
if r.Host != accessURL.Host {
redirect()
return
}

View File

@ -290,11 +290,6 @@ func TestServer(t *testing.T) {
args []string
errContains string
}{
{
name: "NoCertAndKey",
args: []string{"--tls-enable"},
errContains: "--tls-cert-file is required when tls is enabled",
},
{
name: "NoCert",
args: []string{"--tls-enable", "--tls-key-file", key1Path},
@ -373,6 +368,7 @@ func TestServer(t *testing.T) {
},
},
}
defer client.HTTPClient.CloseIdleConnections()
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
@ -527,6 +523,7 @@ func TestServer(t *testing.T) {
},
},
}
defer client.HTTPClient.CloseIdleConnections()
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
@ -541,6 +538,7 @@ func TestServer(t *testing.T) {
name string
httpListener bool
tlsListener bool
redirect bool
accessURL string
// Empty string means no redirect.
expectRedirect string
@ -549,9 +547,17 @@ func TestServer(t *testing.T) {
name: "OK",
httpListener: true,
tlsListener: true,
redirect: true,
accessURL: "https://example.com",
expectRedirect: "https://example.com",
},
{
name: "NoRedirect",
httpListener: true,
tlsListener: true,
accessURL: "https://example.com",
expectRedirect: "",
},
{
name: "NoTLSListener",
httpListener: true,
@ -600,6 +606,9 @@ func TestServer(t *testing.T) {
if c.accessURL != "" {
flags = append(flags, "--access-url", c.accessURL)
}
if c.redirect {
flags = append(flags, "--redirect-to-access-url")
}
root, _ := clitest.New(t, flags...)
pty := ptytest.New(t)
@ -652,20 +661,23 @@ func TestServer(t *testing.T) {
// Verify TLS
if c.tlsListener {
tlsURL, err := url.Parse(tlsAddr)
accessURLParsed, err := url.Parse(c.accessURL)
require.NoError(t, err)
client := codersdk.New(tlsURL)
client := codersdk.New(accessURLParsed)
client.HTTPClient = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return tls.Dial(network, strings.TrimPrefix(tlsAddr, "https://"), &tls.Config{
// nolint:gosec
InsecureSkipVerify: true,
})
},
},
}
defer client.HTTPClient.CloseIdleConnections()
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
@ -837,6 +849,7 @@ func TestServer(t *testing.T) {
},
},
}
defer client.HTTPClient.CloseIdleConnections()
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)

View File

@ -216,6 +216,9 @@ Flags:
"proxy-trusted-headers". e.g.
192.168.1.0/24
Consumes $CODER_PROXY_TRUSTED_ORIGINS
--redirect-to-access-url Specifies whether to redirect requests
that do not match the access URL host.
Consumes $CODER_REDIRECT_TO_ACCESS_URL
--secure-auth-cookie Controls if the 'Secure' property is set
on browser session cookies.
Consumes $CODER_SECURE_AUTH_COOKIE
@ -277,13 +280,6 @@ Flags:
"tls12" or "tls13"
Consumes $CODER_TLS_MIN_VERSION (default
"tls12")
--tls-redirect-http-to-https Whether HTTP requests will be redirected
to the access URL (if it's a https URL
and TLS is enabled). Requests to local IP
addresses are never redirected regardless
of this setting.
Consumes $CODER_TLS_REDIRECT_HTTP
(default true)
--trace Whether application tracing data is
collected. It exports to a backend
configured by environment variables. See:

3
coderd/apidoc/docs.go generated
View File

@ -6057,6 +6057,9 @@ const docTemplate = `{
"rate_limit": {
"$ref": "#/definitions/codersdk.RateLimitConfig"
},
"redirect_to_access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
},
"scim_api_key": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
},

View File

@ -5388,6 +5388,9 @@
"rate_limit": {
"$ref": "#/definitions/codersdk.RateLimitConfig"
},
"redirect_to_access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
},
"scim_api_key": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
},

View File

@ -108,6 +108,7 @@ func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
type DeploymentConfig struct {
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"`
RedirectToAccessURL *DeploymentConfigField[bool] `json:"redirect_to_access_url" typescript:",notnull"`
HTTPAddress *DeploymentConfigField[string] `json:"http_address" typescript:",notnull"`
AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"`
DERP *DERP `json:"derp" typescript:",notnull"`

View File

@ -780,6 +780,17 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \
"value": true
}
},
"redirect_to_access_url": {
"default": true,
"enterprise": true,
"flag": "string",
"hidden": true,
"name": "string",
"secret": true,
"shorthand": "string",
"usage": "string",
"value": true
},
"scim_api_key": {
"default": "string",
"enterprise": true,

View File

@ -2138,6 +2138,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a
"value": true
}
},
"redirect_to_access_url": {
"default": true,
"enterprise": true,
"flag": "string",
"hidden": true,
"name": "string",
"secret": true,
"shorthand": "string",
"usage": "string",
"value": true
},
"scim_api_key": {
"default": "string",
"enterprise": true,
@ -2423,6 +2434,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
| `proxy_trusted_headers` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | |
| `proxy_trusted_origins` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | |
| `rate_limit` | [codersdk.RateLimitConfig](#codersdkratelimitconfig) | false | | |
| `redirect_to_access_url` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
| `scim_api_key` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
| `secure_auth_cookie` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
| `ssh_keygen_algorithm` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |

View File

@ -106,6 +106,8 @@ coder server [flags]
Consumes $CODER_PROXY_TRUSTED_HEADERS
--proxy-trusted-origins strings Origin addresses to respect "proxy-trusted-headers". e.g. 192.168.1.0/24
Consumes $CODER_PROXY_TRUSTED_ORIGINS
--redirect-to-access-url Specifies whether to redirect requests that do not match the access URL host.
Consumes $CODER_REDIRECT_TO_ACCESS_URL
--secure-auth-cookie Controls if the 'Secure' property is set on browser session cookies.
Consumes $CODER_SECURE_AUTH_COOKIE
--ssh-keygen-algorithm string The algorithm to use for generating ssh keys. Accepted values are "ed25519", "ecdsa", or "rsa4096".
@ -134,8 +136,6 @@ coder server [flags]
Consumes $CODER_TLS_KEY_FILE
--tls-min-version string Minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"
Consumes $CODER_TLS_MIN_VERSION (default "tls12")
--tls-redirect-http-to-https Whether HTTP requests will be redirected to the access URL (if it's a https URL and TLS is enabled). Requests to local IP addresses are never redirected regardless of this setting.
Consumes $CODER_TLS_REDIRECT_HTTP (default true)
--trace Whether application tracing data is collected. It exports to a backend configured by environment variables. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md
Consumes $CODER_TRACE_ENABLE
--trace-honeycomb-api-key string Enables trace exporting to Honeycomb.io using the provided API Key.

View File

@ -291,6 +291,7 @@ export interface DangerousConfig {
export interface DeploymentConfig {
readonly access_url: DeploymentConfigField<string>
readonly wildcard_access_url: DeploymentConfigField<string>
readonly redirect_to_access_url: DeploymentConfigField<boolean>
readonly http_address: DeploymentConfigField<string>
readonly autobuild_poll_interval: DeploymentConfigField<number>
readonly derp: DERP