This commit introduces a new test binary responsible for testing TLS authentication. Currently, it covers both remote worker API and Koji API. It tests that the server refuses certificates issued by an untrusted CA or self-signed ones. Also, it tests that the certificate is issued for an allowed domain. TODO: certs with subject alternative name are currently not used in tests. They should work just right, but a proper testing requires more tinkering with OpenSSL than I'm willing to accept at this time
269 lines
7.6 KiB
Go
269 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"flag"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
|
|
"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
|
|
"github.com/osbuild/osbuild-composer/internal/distro/rhel8"
|
|
"github.com/osbuild/osbuild-composer/internal/jobqueue/fsjobqueue"
|
|
"github.com/osbuild/osbuild-composer/internal/kojiapi"
|
|
"github.com/osbuild/osbuild-composer/internal/upload/koji"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/distro"
|
|
"github.com/osbuild/osbuild-composer/internal/rpmmd"
|
|
"github.com/osbuild/osbuild-composer/internal/store"
|
|
"github.com/osbuild/osbuild-composer/internal/weldr"
|
|
"github.com/osbuild/osbuild-composer/internal/worker"
|
|
|
|
"github.com/coreos/go-systemd/activation"
|
|
)
|
|
|
|
const configFile = "/etc/osbuild-composer/osbuild-composer.toml"
|
|
|
|
type connectionConfig struct {
|
|
CACertFile string
|
|
ServerKeyFile string
|
|
ServerCertFile string
|
|
AllowedDomains []string
|
|
}
|
|
|
|
func createTLSConfig(c *connectionConfig) (*tls.Config, error) {
|
|
caCertPEM, err := ioutil.ReadFile(c.CACertFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
roots := x509.NewCertPool()
|
|
ok := roots.AppendCertsFromPEM(caCertPEM)
|
|
if !ok {
|
|
panic("failed to parse root certificate")
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(c.ServerCertFile, c.ServerKeyFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
ClientCAs: roots,
|
|
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
for _, chain := range verifiedChains {
|
|
for _, domain := range c.AllowedDomains {
|
|
if chain[0].VerifyHostname(domain) == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return errors.New("domain not in allowlist")
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func main() {
|
|
var config struct {
|
|
Koji *struct {
|
|
Servers map[string]struct {
|
|
Kerberos *struct {
|
|
Principal string `toml:"principal"`
|
|
KeyTab string `toml:"keytab"`
|
|
} `toml:"kerberos,omitempty"`
|
|
} `toml:"servers"`
|
|
AllowedDomains []string `toml:"allowed_domains"`
|
|
} `toml:"koji"`
|
|
Worker *struct {
|
|
AllowedDomains []string `toml:"allowed_domains"`
|
|
} `toml:"worker,omitempty"`
|
|
}
|
|
var verbose bool
|
|
flag.BoolVar(&verbose, "v", false, "Print access log")
|
|
flag.Parse()
|
|
|
|
_, err := toml.DecodeFile(configFile, &config)
|
|
if err == nil {
|
|
log.Println("Composer configuration:")
|
|
encoder := toml.NewEncoder(log.Writer())
|
|
err := encoder.Encode(&config)
|
|
if err != nil {
|
|
log.Fatalf("Could not print config: %v", err)
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
log.Fatalf("Could not load config file '%s': %v", configFile, err)
|
|
}
|
|
|
|
stateDir, ok := os.LookupEnv("STATE_DIRECTORY")
|
|
if !ok {
|
|
log.Fatal("STATE_DIRECTORY is not set. Is the service file missing StateDirectory=?")
|
|
}
|
|
|
|
listeners, err := activation.ListenersWithNames()
|
|
if err != nil {
|
|
log.Fatalf("Could not get listening sockets: " + err.Error())
|
|
}
|
|
|
|
if _, exists := listeners["osbuild-composer.socket"]; !exists {
|
|
log.Fatalf("osbuild-composer.socket doesn't exist")
|
|
}
|
|
|
|
composerListeners := listeners["osbuild-composer.socket"]
|
|
|
|
if len(composerListeners) != 2 && len(composerListeners) != 3 {
|
|
log.Fatalf("Unexpected number of listening sockets (%d), expected 2 or 3", len(composerListeners))
|
|
}
|
|
|
|
weldrListener := composerListeners[0]
|
|
jobListener := composerListeners[1]
|
|
|
|
cacheDirectory, ok := os.LookupEnv("CACHE_DIRECTORY")
|
|
if !ok {
|
|
log.Fatal("CACHE_DIRECTORY is not set. Is the service file missing CacheDirectory=?")
|
|
}
|
|
|
|
rpm := rpmmd.NewRPMMD(path.Join(cacheDirectory, "rpmmd"), "/usr/libexec/osbuild-composer/dnf-json")
|
|
|
|
distros, err := distro.NewRegistry(fedora31.New(), fedora32.New(), rhel8.New())
|
|
if err != nil {
|
|
log.Fatalf("Error loading distros: %v", err)
|
|
}
|
|
|
|
distribution, beta, err := distros.FromHost()
|
|
if err != nil {
|
|
log.Fatalf("Could not determine distro from host: " + err.Error())
|
|
}
|
|
|
|
arch, err := distribution.GetArch(common.CurrentArch())
|
|
if err != nil {
|
|
log.Fatalf("Host distro does not support host architecture: " + err.Error())
|
|
}
|
|
|
|
// TODO: refactor to be more generic
|
|
name := distribution.Name()
|
|
if beta {
|
|
name += "-beta"
|
|
}
|
|
|
|
repoMap, err := rpmmd.LoadRepositories([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"}, name)
|
|
if err != nil {
|
|
log.Fatalf("Could not load repositories for %s: %v", distribution.Name(), err)
|
|
}
|
|
|
|
var logger *log.Logger
|
|
if verbose {
|
|
logger = log.New(os.Stdout, "", 0)
|
|
}
|
|
|
|
store := store.New(&stateDir, arch, logger)
|
|
|
|
queueDir := path.Join(stateDir, "jobs")
|
|
err = os.Mkdir(queueDir, 0700)
|
|
if err != nil && !os.IsExist(err) {
|
|
log.Fatalf("cannot create queue directory: %v", err)
|
|
}
|
|
|
|
jobs, err := fsjobqueue.New(queueDir, []string{"osbuild"})
|
|
if err != nil {
|
|
log.Fatalf("cannot create jobqueue: %v", err)
|
|
}
|
|
|
|
artifactsDir := path.Join(stateDir, "artifacts")
|
|
err = os.Mkdir(artifactsDir, 0755)
|
|
if err != nil && !os.IsExist(err) {
|
|
log.Fatalf("cannot create artifacts directory: %v", err)
|
|
}
|
|
|
|
compatOutputDir := path.Join(stateDir, "outputs")
|
|
|
|
workers := worker.NewServer(logger, jobs, artifactsDir)
|
|
weldrAPI := weldr.New(rpm, arch, distribution, repoMap[common.CurrentArch()], logger, store, workers, compatOutputDir)
|
|
|
|
go func() {
|
|
err := workers.Serve(jobListener)
|
|
common.PanicOnError(err)
|
|
}()
|
|
|
|
// Optionally run Koji API
|
|
if kojiListeners, exists := listeners["osbuild-composer-koji.socket"]; exists {
|
|
if config.Koji == nil {
|
|
log.Fatal("koji not configured in the config file")
|
|
}
|
|
kojiServers := make(map[string]koji.GSSAPICredentials)
|
|
for server, creds := range config.Koji.Servers {
|
|
if creds.Kerberos == nil {
|
|
// For now we only support Kerberos authentication.
|
|
continue
|
|
}
|
|
kojiServers[server] = koji.GSSAPICredentials{
|
|
Principal: creds.Kerberos.Principal,
|
|
KeyTab: creds.Kerberos.KeyTab,
|
|
}
|
|
}
|
|
|
|
kojiServer := kojiapi.NewServer(logger, workers, rpm, distros, kojiServers)
|
|
|
|
tlsConfig, err := createTLSConfig(&connectionConfig{
|
|
CACertFile: "/etc/osbuild-composer/ca-crt.pem",
|
|
ServerKeyFile: "/etc/osbuild-composer/composer-key.pem",
|
|
ServerCertFile: "/etc/osbuild-composer/composer-crt.pem",
|
|
AllowedDomains: config.Koji.AllowedDomains,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("TLS configuration cannot be created: " + err.Error())
|
|
}
|
|
|
|
if len(kojiListeners) != 1 {
|
|
// Use Fatal to call os.Exit with non-zero return value
|
|
log.Fatal("The osbuild-composer-koji.socket unit is misconfigured. It should contain only one socket.")
|
|
}
|
|
kojiListener := tls.NewListener(kojiListeners[0], tlsConfig)
|
|
|
|
go func() {
|
|
err = kojiServer.Serve(kojiListener)
|
|
|
|
// If the koji server fails, take down the whole process, not just a single goroutine
|
|
log.Fatal("osbuild-composer-koji.socket failed: ", err)
|
|
}()
|
|
}
|
|
|
|
if remoteWorkerListeners, exists := listeners["osbuild-remote-worker.socket"]; exists {
|
|
for _, listener := range remoteWorkerListeners {
|
|
log.Printf("Starting remote listener\n")
|
|
|
|
if config.Worker == nil {
|
|
log.Fatal("remote worker not configured in the config file")
|
|
}
|
|
|
|
tlsConfig, err := createTLSConfig(&connectionConfig{
|
|
CACertFile: "/etc/osbuild-composer/ca-crt.pem",
|
|
ServerKeyFile: "/etc/osbuild-composer/composer-key.pem",
|
|
ServerCertFile: "/etc/osbuild-composer/composer-crt.pem",
|
|
AllowedDomains: config.Worker.AllowedDomains,
|
|
})
|
|
|
|
if err != nil {
|
|
log.Fatalf("TLS configuration cannot be created: " + err.Error())
|
|
}
|
|
|
|
listener := tls.NewListener(listener, tlsConfig)
|
|
go func() {
|
|
err := workers.Serve(listener)
|
|
common.PanicOnError(err)
|
|
}()
|
|
}
|
|
}
|
|
|
|
err = weldrAPI.Serve(weldrListener)
|
|
common.PanicOnError(err)
|
|
|
|
}
|