mirror of https://github.com/coder/coder.git
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:
parent
e27f7accd7
commit
b9b402cd0c
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 | | |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue