Skip to content

Commit

Permalink
Merge pull request #39 from madflojo/2waytls
Browse files Browse the repository at this point in the history
Adding m-TLS for Authentication
  • Loading branch information
madflojo authored Jul 28, 2022
2 parents f8bc1d6 + 4fa43e1 commit 7f63497
Show file tree
Hide file tree
Showing 7 changed files with 462 additions and 11 deletions.
33 changes: 24 additions & 9 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package app

import (
"context"
"crypto/tls"
"database/sql"
"fmt"
// MySQL Database Driver
Expand All @@ -23,6 +22,7 @@ import (
"github.com/madflojo/tarmac/pkg/callbacks/metrics"
sqlstore "github.com/madflojo/tarmac/pkg/callbacks/sql"
"github.com/madflojo/tarmac/pkg/telemetry"
"github.com/madflojo/tarmac/pkg/tlsconfig"
"github.com/madflojo/tarmac/pkg/wasm"
"github.com/madflojo/tasks"
"github.com/prometheus/client_golang/prometheus/promhttp"
Expand Down Expand Up @@ -223,13 +223,29 @@ func Run(c *viper.Viper) error {

// Setup TLS Configuration
if cfg.GetBool("enable_tls") {
srv.httpServer.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
tlsCfg := tlsconfig.New()

// Load Certs from file
err := tlsCfg.CertsFromFile(cfg.GetString("cert_file"), cfg.GetString("key_file"))
if err != nil {
return fmt.Errorf("unable to configure HTTPS server with certificate and key - %s", err)
}

// Load CA enabling m-TLS
if cfg.GetString("ca_file") != "" {
err := tlsCfg.CAFromFile(cfg.GetString("ca_file"))
if err != nil {
return fmt.Errorf("unable to configure HTTPS server with provided client certificate authority - %s", err)
}

// Set to ask but ignore client certs
if cfg.GetBool("ignore_client_cert") {
tlsCfg.IgnoreClientCert()
}
}

// Generate TLS config and assign to HTTP Server
srv.httpServer.TLSConfig = tlsCfg.Generate()
}

// Kick off Graceful Shutdown Go Routine
Expand Down Expand Up @@ -418,9 +434,8 @@ func Run(c *viper.Viper) error {
srv.httpRouter.HEAD("/", srv.middleware(srv.WASMHandler))

// Start HTTP Listener
log.Infof("Starting Listener on %s", cfg.GetString("listen_addr"))
log.Infof("Starting HTTP Listener on %s", cfg.GetString("listen_addr"))
if cfg.GetBool("enable_tls") {
log.Infof("Using Certificate: %s Key: %s", cfg.GetString("cert_file"), cfg.GetString("key_file"))
err := srv.httpServer.ListenAndServeTLS(cfg.GetString("cert_file"), cfg.GetString("key_file"))
if err != nil {
if err == http.ErrServerClosed {
Expand Down
175 changes: 175 additions & 0 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"context"
"crypto/tls"
"github.com/madflojo/tarmac/pkg/tlsconfig"
"github.com/madflojo/testcerts"
"github.com/spf13/viper"
_ "github.com/spf13/viper/remote"
Expand Down Expand Up @@ -341,3 +342,177 @@ func TestRunningTLSServer(t *testing.T) {
})

}

func TestRunningMTLSServer(t *testing.T) {
// Create Test Certs
err := testcerts.GenerateCertsToFile("/tmp/cert", "/tmp/key")
if err != nil {
t.Errorf("Failed to create certs - %s", err)
t.FailNow()
}
defer os.Remove("/tmp/cert")
defer os.Remove("/tmp/key")

// Setup TLS Config
tlsCfg := tlsconfig.New()
err = tlsCfg.CertsFromFile("/tmp/cert", "/tmp/key")
if err != nil {
t.Fatalf("Failed to load certs - %s", err)
}

tlsCfg.IgnoreHostValidation()

// Disable Host Checking globally
http.DefaultTransport.(*http.Transport).TLSClientConfig = tlsCfg.Generate()

// Setup Config
cfg := viper.New()
cfg.Set("disable_logging", true)
cfg.Set("trace", true)
cfg.Set("debug", true)
cfg.Set("enable_tls", true)
cfg.Set("cert_file", "/tmp/cert")
cfg.Set("ca_file", "/tmp/cert")
cfg.Set("key_file", "/tmp/key")
cfg.Set("kvstore_type", "cassandra")
cfg.Set("cassandra_hosts", []string{"cassandra-primary", "cassandra"})
cfg.Set("cassandra_keyspace", "tarmac")
cfg.Set("enable_kvstore", true)
cfg.Set("use_consul", false)
cfg.Set("listen_addr", "localhost:9000")
cfg.Set("config_watch_interval", 1)
cfg.Set("enable_sql", true)
cfg.Set("sql_type", "mysql")
cfg.Set("sql_dsn", "root:example@tcp(mysql:3306)/example")
err = cfg.AddRemoteProvider("consul", "consul:8500", "tarmac/config")
if err != nil {
t.Fatalf("Failed to create Consul config provider - %s", err)
}
cfg.SetConfigType("json")
_ = cfg.ReadRemoteConfig()

// Start Server in goroutine
go func() {
err := Run(cfg)
if err != nil && err != ErrShutdown {
t.Errorf("Run unexpectedly stopped - %s", err)
}
}()
// Clean up
defer Stop()

// Wait for app to start
time.Sleep(15 * time.Second)

t.Run("Check Health HTTP Handler", func(t *testing.T) {
r, err := http.Get("https://localhost:9000/health")
if err != nil {
t.Errorf("Unexpected error when requesting health status - %s", err)
t.FailNow()
}
defer r.Body.Close()
if r.StatusCode != 200 {
t.Errorf("Unexpected http status code when checking health - %d", r.StatusCode)
}
})

t.Run("Check Ready HTTP Handler", func(t *testing.T) {
r, err := http.Get("https://localhost:9000/ready")
if err != nil {
t.Errorf("Unexpected error when requesting ready status - %s", err)
t.FailNow()
}
defer r.Body.Close()
if r.StatusCode != 200 {
t.Errorf("Unexpected http status code when checking readiness - %d", r.StatusCode)
}
})

// Kill the DB sessions for unhappy path testing
kv.Close()

t.Run("Check Ready HTTP Handler with DB Stopped", func(t *testing.T) {
r, err := http.Get("https://localhost:9000/ready")
if err != nil {
t.Errorf("Unexpected error when requesting ready status - %s", err)
t.FailNow()
}
defer r.Body.Close()
if r.StatusCode != 503 {
t.Errorf("Unexpected http status code when checking readiness - %d", r.StatusCode)
}
})

t.Run("Check if Remote config was read", func(t *testing.T) {
if !cfg.GetBool("from_consul") {
t.Errorf("Did not fetch config from consul")
}
})

}

func TestRunningFailMTLSServer(t *testing.T) {
// Create Test Certs
err := testcerts.GenerateCertsToFile("/tmp/cert", "/tmp/key")
if err != nil {
t.Errorf("Failed to create certs - %s", err)
t.FailNow()
}
defer os.Remove("/tmp/cert")
defer os.Remove("/tmp/key")

// Setup TLS Config
tlsCfg := tlsconfig.New()
tlsCfg.IgnoreHostValidation()

// Disable Host Checking globally
http.DefaultTransport.(*http.Transport).TLSClientConfig = tlsCfg.Generate()

// Setup Config
cfg := viper.New()
cfg.Set("disable_logging", true)
cfg.Set("trace", true)
cfg.Set("debug", true)
cfg.Set("enable_tls", true)
cfg.Set("cert_file", "/tmp/cert")
cfg.Set("ca_file", "/tmp/cert")
cfg.Set("key_file", "/tmp/key")
cfg.Set("kvstore_type", "cassandra")
cfg.Set("cassandra_hosts", []string{"cassandra-primary", "cassandra"})
cfg.Set("cassandra_keyspace", "tarmac")
cfg.Set("enable_kvstore", true)
cfg.Set("use_consul", false)
cfg.Set("listen_addr", "localhost:9000")
cfg.Set("config_watch_interval", 1)
cfg.Set("enable_sql", true)
cfg.Set("sql_type", "mysql")
cfg.Set("sql_dsn", "root:example@tcp(mysql:3306)/example")
err = cfg.AddRemoteProvider("consul", "consul:8500", "tarmac/config")
if err != nil {
t.Fatalf("Failed to create Consul config provider - %s", err)
}
cfg.SetConfigType("json")
_ = cfg.ReadRemoteConfig()

// Start Server in goroutine
go func() {
err := Run(cfg)
if err != nil && err != ErrShutdown {
t.Errorf("Run unexpectedly stopped - %s", err)
}
}()
// Clean up
defer Stop()

// Wait for app to start
time.Sleep(15 * time.Second)

t.Run("Check Health HTTP Handler", func(t *testing.T) {
r, err := http.Get("https://localhost:9000/health")
if err == nil {
defer r.Body.Close()
t.Errorf("Unexpected success when requesting health status")
t.FailNow()
}
})
}
3 changes: 2 additions & 1 deletion docs/running-tarmac/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ When using Environment Variables, all configurations are prefixed with `APP_`. T
| `APP_DISABLE_LOGGING` | `disable_logging` | `bool` | Disable all logging |
| `APP_CERT_FILE` | `cert_file` | `string` | Certificate File Path \(i.e. `/some/path/cert.crt`\) |
| `APP_KEY_FILE` | `key_file` | `string` | Key File Path \(i.e. `/some/path/cert.key`\) |
| `APP_CA_FILE` | `ca_file` | `string` | Certificate Authority Bundle File Path \(i.e `/some/path/ca.pem`\). When defined, enables mutual-TLS authentication |
| `APP_IGNORE_CLIENT_CERT` | `ignore_client_cert` | `string` | When defined will disable Client Cert validation for m-TLS authentication |
| `APP_WASM_FUNCTION` | `wasm_function` | `string` | Path and Filename of the WASM Function to execute \(Default: `/functions/tarmac.wasm`\) |
| `APP_ENABLE_PPROF` | `enable_pprof` | `bool` | Enable PProf Collection HTTP end-points |
| `APP_ENABLE_KVSTORE` | `enable_kvstore` | `bool` | Enable the KV Store |
Expand Down Expand Up @@ -56,7 +58,6 @@ The below options are used to configure scheduled tasks.
| :--- | :--- | :--- |
| `interval` | `int` | Interval (in seconds) task execution should run (recurring) |
| `wasm_function` | `string` | Path and Filename of the WASM Function to execute |
| `headers` | `map[string]string` | Custom headers applied to the ServerRequest provided to WASM functions during execution |

## Consul Format

Expand Down
2 changes: 1 addition & 1 deletion docs/wasm-functions/supported-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The below table outlines languages that are fully supported.
| Language | waPC Guest Library | Caveats |
| :--- | :--- | :--- |
| AssemblyScript | [https://github.com/wapc/as-guest](https://github.com/wapc/as-guest) | |
| Go | [https://github.com/wapc/wapc-guest-tinygo](https://github.com/wapc/wapc-guest-tinygo) | Currently only supports TinyGo versions pre-0.18.0 |
| Go | [https://github.com/wapc/wapc-guest-tinygo](https://github.com/wapc/wapc-guest-tinygo) | |
| Rust | [https://github.com/wapc/wapc-guest-rust](https://github.com/wapc/wapc-guest-rust) | |
| Swift | [https://github.com/wapc/wapc-guest-swift](https://github.com/wapc/wapc-guest-swift) | |
| Zig | [https://github.com/wapc/wapc-guest-zig](https://github.com/wapc/wapc-guest-zig) | |
Expand Down
3 changes: 3 additions & 0 deletions pkg/telemetry/telemetry.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/*
Telemetry is an internal Tarmac package used to initialize system metrics.
*/
package telemetry

import (
Expand Down
116 changes: 116 additions & 0 deletions pkg/tlsconfig/tlsconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
Package tlsconfig is a helper package used to create TLS configuration that adheres to best practices.
This package aims to write less repetitive code when creating TLS configurations. Opening certs, bundling certificate
authorities, configuring ciphers, etc. Just use this package to save yourself some headaches.
// Create an instance of config
cfg := tlsconfig.New()
// Load Certificates
err := cfg.CertsFromFile(cert, key)
if err != nil {
// do something
}
// Disable Host Validation
cfg.IgnoreHostValidation()
// Use the Config
h := &http.Server{TLSConfig: cfg.Generate()}
*/
package tlsconfig

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
)

// Config is used to create an instance of the configuration helper. It holds the basic TLS configuration for
// repeated generation.
type Config struct {
config tls.Config
}

// New will create a new config instance with basic TLS best practices pre-defined.
func New() *Config {
c := Config{
config: tls.Config{
// Restrict to use TLS 1.2 as the minimum TLS versions
MinVersion: tls.VersionTLS12,
// Restrict Cipher Suites
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
},
},
}
return &c
}

// CertsFromFile will read the certificate and key file and create an X509 KeyPair loaded as
// Certificates. The files must contain PEM encoded data. The certificate file may contain
// intermediate certificates following the leaf certificate to form a certificate chain.
func (c *Config) CertsFromFile(cert, key string) error {
if cert == "" || key == "" {
return fmt.Errorf("cert and key cannot be empty")
}

pair, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return fmt.Errorf("unable to load keypair - %s", err)
}

c.config.Certificates = append(c.config.Certificates, pair)
return nil
}

// CAFromFile will read the PEM encoded certificate authority file and register the
// certificate as an authority for Client Authentication. This function is for m-TLS
// configuration at the server level. By default, this function sets Client
// Authentication to Require and Verify the Certificate.
func (c *Config) CAFromFile(ca string) error {
c.config.ClientAuth = tls.RequireAndVerifyClientCert

if ca == "" {
return fmt.Errorf("ca cannot be empty")
}

b, err := ioutil.ReadFile(ca)
if err != nil {
return fmt.Errorf("unable to read ca file - %s", err)
}

pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(b) {
return fmt.Errorf("unable to load ca certificate - %s", err)
}

c.config.ClientCAs = pool
return nil
}

// IgnoreClientCert will set client certificate authentication to verify the certificate
// only if provided. Otherwise, if no certificate is provided, the client will still be allowed.
func (c *Config) IgnoreClientCert() {
c.config.ClientAuth = tls.VerifyClientCertIfGiven
}

// IgnoreHostValidation will turn off the hostname validation of certificates. This
// setting is dangerous and should only be used in testing.
func (c *Config) IgnoreHostValidation() {
c.config.InsecureSkipVerify = true
}

// Generate will create a TLS configuration type based on the defaults and settings called.
// Users can run this multiple times to produce the same configuration.
func (c *Config) Generate() *tls.Config {
return c.config.Clone()
}
Loading

0 comments on commit 7f63497

Please sign in to comment.