Added optional HTTPS client certificate authentication to https2http and https2https

pull/4722/head
Leonardo Guermandi Curvelo 2025-03-22 22:27:38 -03:00
parent 773169e0c4
commit acb53acb2e
6 changed files with 327 additions and 85 deletions

View File

@ -89,6 +89,7 @@ frp also offers a P2P connect mode.
* [HTTP X-Forwarded-For](#http-x-forwarded-for)
* [Proxy Protocol](#proxy-protocol)
* [Require HTTP Basic Auth (Password) for Web Services](#require-http-basic-auth-password-for-web-services)
* [Require HTTPS client certificate for Web Services](#require-https-client-certificate-for-web-services)
* [Custom Subdomain Names](#custom-subdomain-names)
* [URL Routing](#url-routing)
* [TCP Port Multiplexing](#tcp-port-multiplexing)
@ -1046,11 +1047,11 @@ You can enable Proxy Protocol support in nginx to expose user's real IP in HTTP
### Require HTTP Basic Auth (Password) for Web Services
Anyone who can guess your tunnel URL can access your local web server unless you protect it with a password.
Anyone who can guess your HTTP tunnel URL can access your local web server unless you protect it with a password.
This enforces HTTP Basic Auth on all requests with the username and password specified in frpc's configure file.
It can only be enabled when proxy type is http.
It can only be enabled when proxy type is http. For https protection, see [Require HTTPS client certificate for Web Services](#require-https-client-certificate-for-web-services).
```toml
# frpc.toml
@ -1066,6 +1067,44 @@ httpPassword = "abc"
Visit `http://test.example.com` in the browser and now you are prompted to enter the username and password.
### Require HTTPS client certificate for Web Services
Anyone who can guess your HTTPS tunnel URL can access your local web server unless you protect it with a [Client Certificate](https://en.wikipedia.org/wiki/Transport_Layer_Security#Client-authenticated_TLS_handshake).
This [mutual authentication](https://en.wikipedia.org/wiki/Mutual_authentication) validates the HTTPS client's certificate on all requests, with each accepted certificate file specified in frpc's configure file.
It can only be enabled when proxy type is https. For http protection, see [Require HTTP Basic Auth (Password) for Web Services](#require-http-basic-auth-password-for-web-services).
```toml
[[proxies]]
name = "web"
type = "https"
customDomains = ["test.example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:80"
crtPath = server.crt"
keyPath = "key.pem"
clientCertificates = ["authorizedClient_cert.pem"]
```
In this situation, the client certificate can be self-signed without any detriment to security. Multiple certificates can be generated and allowed to access the service.
Generate a .cert file **to use with frpc**, and a corresponding .pfx file **to install on Windows, Linux, or your browser**. The following instructions require the OpenSSL binary installed:
```bash
# Windows
openssl req -x509 -newkey rsa:4096 -keyout authorizedClient_cert.pem -out authorizedClient_key.pem -sha256 -days 3650 -nodes -subj "/CN=FRP Authentication Certificate"
type authorizedClient_cert.pem authorizedClient_key.pem > authorizedClient.pem
openssl pkcs12 -export -in authorizedClient.pem -out authorizedClient.pfx -name "FRP Authentication Certificate"
# Linux
openssl req -x509 -newkey rsa:4096 -keyout authorizedClient_cert.pem -out authorizedClient_key.pem -sha256 -days 3650 -nodes -subj "/CN=FRP Authentication Certificate"
cat authorizedClient_cert.pem authorizedClient_key.pem > authorizedClient.pem
openssl pkcs12 -export -in authorizedClient.pem -out authorizedClient.pfx -name "FRP Authentication Certificate"
```
### Custom Subdomain Names
It is convenient to use `subdomain` configure for http and https types when many people share one frps server.

View File

@ -116,13 +116,14 @@ type HTTPProxyPluginOptions struct {
func (o *HTTPProxyPluginOptions) Complete() {}
type HTTPS2HTTPPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
ClientCertificates []string `json:"clientCertificates,omitempty"`
}
func (o *HTTPS2HTTPPluginOptions) Complete() {
@ -130,13 +131,14 @@ func (o *HTTPS2HTTPPluginOptions) Complete() {
}
type HTTPS2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
ClientCertificates []string `json:"clientCertificates,omitempty"`
}
func (o *HTTPS2HTTPSPluginOptions) Complete() {

View File

@ -19,6 +19,7 @@ package plugin
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
stdlog "log"
@ -91,6 +92,33 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return nil, fmt.Errorf("gen TLS config error: %v", err)
}
if len(p.opts.ClientCertificates) > 0 {
certs, err := transport.LoadCertificatesFromFiles(p.opts.ClientCertificates)
if err != nil {
return nil, fmt.Errorf("loading Client Certificates failed: %v", err)
}
tlsConfig.ClientAuth = tls.RequireAnyClientCert
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no client certificate provided")
}
clientCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("failed to parse client certificate: %w", err)
}
for _, allowedCert := range certs {
if clientCert.Equal(allowedCert) {
return nil //match found, accept
}
}
return fmt.Errorf("client certificate not recognized")
}
}
p.s = &http.Server{
Handler: handler,
ReadHeaderTimeout: 60 * time.Second,

View File

@ -19,6 +19,7 @@ package plugin
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
stdlog "log"
@ -97,6 +98,33 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return nil, fmt.Errorf("gen TLS config error: %v", err)
}
if len(p.opts.ClientCertificates) > 0 {
certs, err := transport.LoadCertificatesFromFiles(p.opts.ClientCertificates)
if err != nil {
return nil, fmt.Errorf("loading Client Certificates failed: %v", err)
}
tlsConfig.ClientAuth = tls.RequireAnyClientCert
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no client certificate provided")
}
clientCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("failed to parse client certificate: %w", err)
}
for _, allowedCert := range certs {
if clientCert.Equal(allowedCert) {
return nil //match found, accept
}
}
return fmt.Errorf("client certificate not recognized")
}
}
p.s = &http.Server{
Handler: handler,
ReadHeaderTimeout: 60 * time.Second,

View File

@ -20,6 +20,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"os"
)
@ -140,3 +141,28 @@ func NewRandomPrivateKey() ([]byte, error) {
})
return keyPEM, nil
}
func LoadCertificatesFromFiles(certFiles []string) ([]*x509.Certificate, error) {
var certificates []*x509.Certificate
for _, file := range certFiles {
certPEM, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("failed to read certificate file %s: %w", file, err)
}
block, _ := pem.Decode(certPEM)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate file %s: %w", file, err)
}
certificates = append(certificates, cert)
}
return certificates, nil
}

View File

@ -236,67 +236,126 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
Ensure()
})
ginkgo.It("https2http", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
ginkgo.Describe("https2http", func() {
ginkgo.It("without client certificate requirement", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
vhostHTTPSPort = %d
`, vhostHTTPSPort)
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
vhostHTTPSPort = %d
`, vhostHTTPSPort)
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "https2http"
type = "https"
customDomains = ["example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:%d"
crtPath = "%s"
keyPath = "%s"
`, localPort, crtPath, keyPath)
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "https2http"
type = "https"
customDomains = ["example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:%d"
crtPath = "%s"
keyPath = "%s"
`, localPort, crtPath, keyPath)
f.RunProcesses([]string{serverConf}, []string{clientConf})
f.RunProcesses([]string{serverConf}, []string{clientConf})
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
)
f.RunServer("", localServer)
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
)
f.RunServer("", localServer)
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
})
}).
ExpectResp([]byte("test")).
Ensure()
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
})
}).
ExpectResp([]byte("test")).
Ensure()
})
ginkgo.It("with client certificate requirement", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
artifacts, err = generator.Generate("127.0.0.1")
framework.ExpectNoError(err)
clientCrtPath := f.WriteTempFile("client.crt", string(artifacts.Cert))
clientKeyPath := f.WriteTempFile("client.key", string(artifacts.Key))
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
vhostHTTPSPort = %d
`, vhostHTTPSPort)
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "https2http"
type = "https"
customDomains = ["example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:%d"
crtPath = "%s"
keyPath = "%s"
clientCertificates = ["%s"]
`, localPort, crtPath, keyPath, clientCrtPath)
f.RunProcesses([]string{serverConf}, []string{clientConf})
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
)
f.RunServer("", localServer)
clientCertificate, err := tls.LoadX509KeyPair(clientCrtPath, clientKeyPath)
framework.ExpectNoError(err)
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
Certificates: []tls.Certificate{clientCertificate},
})
}).
ExpectResp([]byte("test")).
Ensure()
})
})
ginkgo.It("https2https", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
ginkgo.Describe("https2https", func() {
ginkgo.It("without client certificate requirement", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
vhostHTTPSPort = %d
`, vhostHTTPSPort)
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "https2https"
type = "https"
@ -308,27 +367,87 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
keyPath = "%s"
`, localPort, crtPath, keyPath)
f.RunProcesses([]string{serverConf}, []string{clientConf})
f.RunProcesses([]string{serverConf}, []string{clientConf})
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
framework.ExpectNoError(err)
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
httpserver.WithTLSConfig(tlsConfig),
)
f.RunServer("", localServer)
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
framework.ExpectNoError(err)
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
httpserver.WithTLSConfig(tlsConfig),
)
f.RunServer("", localServer)
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
})
}).
ExpectResp([]byte("test")).
Ensure()
})
ginkgo.It("with client certificate requirement", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("server.key", string(artifacts.Key))
artifacts, err = generator.Generate("127.0.0.1")
framework.ExpectNoError(err)
clientCrtPath := f.WriteTempFile("client.crt", string(artifacts.Cert))
clientKeyPath := f.WriteTempFile("client.key", string(artifacts.Key))
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
vhostHTTPSPort = %d
`, vhostHTTPSPort)
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "https2https"
type = "https"
customDomains = ["example.com"]
[proxies.plugin]
type = "https2https"
localAddr = "127.0.0.1:%d"
crtPath = "%s"
keyPath = "%s"
clientCertificates = ["%s"]
`, localPort, crtPath, keyPath, clientCrtPath)
f.RunProcesses([]string{serverConf}, []string{clientConf})
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
framework.ExpectNoError(err)
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
httpserver.WithTLSConfig(tlsConfig),
)
f.RunServer("", localServer)
clientCertificate, err := tls.LoadX509KeyPair(clientCrtPath, clientKeyPath)
framework.ExpectNoError(err)
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
Certificates: []tls.Certificate{clientCertificate},
})
}).
ExpectResp([]byte("test")).
Ensure()
})
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
})
}).
ExpectResp([]byte("test")).
Ensure()
})
ginkgo.Describe("http2http", func() {