diff --git a/Makefile b/Makefile index ec5cc1875..7d190ecb1 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,29 @@ install: cp distribution/*.socket /etc/systemd/system/ systemctl daemon-reload +.PHONY: ca +ca: +ifneq (/etc/osbuild-composer/ca-key.pem/etc/osbuild-composer/ca-crt.pem,$(wildcard /etc/osbuild-composer/ca-key.pem)$(wildcard /etc/osbuild-composer/ca-crt.pem)) + @echo CA key or certificate file is missing, generating a new pair... + - mkdir -p /etc/osbuild-composer + openssl req -new -nodes -x509 -days 365 -keyout /etc/osbuild-composer/ca-key.pem -out /etc/osbuild-composer/ca-crt.pem -subj "/CN=osbuild.org" +else + @echo CA key and certificate files already exist, skipping... +endif + +.PHONY: composer-key-pair +composer-key-pair: ca + openssl genrsa -out /etc/osbuild-composer/composer-key.pem 2048 + openssl req -new -sha256 -key /etc/osbuild-composer/composer-key.pem -out /etc/osbuild-composer/composer-csr.pem -subj "/CN=localhost" # TODO: we need to generate certificates with another hostname + openssl x509 -req -in /etc/osbuild-composer/composer-csr.pem -CA /etc/osbuild-composer/ca-crt.pem -CAkey /etc/osbuild-composer/ca-key.pem -CAcreateserial -out /etc/osbuild-composer/composer-crt.pem + chown _osbuild-composer:_osbuild-composer /etc/osbuild-composer/composer-key.pem /etc/osbuild-composer/composer-csr.pem /etc/osbuild-composer/composer-crt.pem + +.PHONY: worker-key-pair +worker-key-pair: ca + openssl genrsa -out /etc/osbuild-composer/worker-key.pem 2048 + openssl req -new -sha256 -key /etc/osbuild-composer/worker-key.pem -out /etc/osbuild-composer/worker-csr.pem -subj "/CN=localhost" + openssl x509 -req -in /etc/osbuild-composer/worker-csr.pem -CA /etc/osbuild-composer/ca-crt.pem -CAkey /etc/osbuild-composer/ca-key.pem -CAcreateserial -out /etc/osbuild-composer/worker-crt.pem + .PHONY: tarball tarball: git archive --prefix=$(PACKAGE_NAME)-$(VERSION)/ --format=tar.gz HEAD > $(PACKAGE_NAME)-$(VERSION).tar.gz diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go index c5db90fd2..ee9f1eb77 100644 --- a/cmd/osbuild-composer/main.go +++ b/cmd/osbuild-composer/main.go @@ -1,7 +1,10 @@ package main import ( + "crypto/tls" + "crypto/x509" "flag" + "io/ioutil" "log" "os" "runtime" @@ -30,6 +33,35 @@ func currentArch() string { } } +type connectionConfig struct { + CACertFile string + ServerKeyFile string + ServerCertFile 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, + }, nil +} + func main() { var verbose bool flag.BoolVar(&verbose, "v", false, "Print access log") @@ -37,17 +69,23 @@ func main() { stateDir := "/var/lib/osbuild-composer" - listeners, err := activation.Listeners() + listeners, err := activation.ListenersWithNames() if err != nil { log.Fatalf("Could not get listening sockets: " + err.Error()) } - if len(listeners) != 2 && len(listeners) != 3 { - log.Fatalf("Unexpected number of listening sockets (%d), expected 2 or 3", len(listeners)) + if _, exists := listeners["osbuild-composer.socket"]; !exists { + log.Fatalf("osbuild-composer.socket doesn't exist") } - weldrListener := listeners[0] - jobListener := listeners[1] + 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] rpm := rpmmd.NewRPMMD() distros := distro.NewRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"}) @@ -71,10 +109,29 @@ func main() { // Optionally run RCM API as well as Weldr API if len(listeners) == 3 { - rcmListener := listeners[2] + rcmListener := composerListeners[2] rcmAPI := rcm.New(logger, store, rpmmd.NewRPMMD()) go rcmAPI.Serve(rcmListener) } + if remoteWorkerListeners, exists := listeners["osbuild-remote-worker.socket"]; exists { + for _, listener := range remoteWorkerListeners { + log.Printf("Starting remote listener\n") + + 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", + }) + + if err != nil { + log.Fatalf("TLS configuration cannot be created: " + err.Error()) + } + + listener := tls.NewListener(listener, tlsConfig) + go jobAPI.Serve(listener) + } + } + weldrAPI.Serve(weldrListener) } diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go index ebadd1514..3a61b86bb 100644 --- a/cmd/osbuild-worker/main.go +++ b/cmd/osbuild-worker/main.go @@ -3,10 +3,14 @@ package main import ( "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/json" "errors" + "flag" "fmt" "io" + "io/ioutil" "log" "net" "net/http" @@ -15,19 +19,80 @@ import ( "github.com/osbuild/osbuild-composer/internal/jobqueue" ) +const RemoteWorkerPort = 8700 + type ComposerClient struct { - client *http.Client + client *http.Client + hostname string } -func NewClient() *ComposerClient { +type connectionConfig struct { + CACertFile string + ClientKeyFile string + ClientCertFile string +} + +func createTLSConfig(config *connectionConfig) (*tls.Config, error) { + caCertPEM, err := ioutil.ReadFile(config.CACertFile) + if err != nil { + return nil, err + } + + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(caCertPEM) + if !ok { + return nil, errors.New("failed to append root certificate") + } + + cert, err := tls.LoadX509KeyPair(config.ClientCertFile, config.ClientKeyFile) + if err != nil { + return nil, err + } + + return &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + }, nil +} + +func newConnection(remoteAddress string) (net.Conn, error) { + if remoteAddress != "" { + conf, err := createTLSConfig(&connectionConfig{ + CACertFile: "/etc/osbuild-composer/ca-crt.pem", + ClientKeyFile: "/etc/osbuild-composer/worker-key.pem", + ClientCertFile: "/etc/osbuild-composer/worker-crt.pem", + }) + + if err != nil { + return nil, err + } + + address := fmt.Sprintf("%s:%d", remoteAddress, RemoteWorkerPort) + + return tls.Dial("tcp", address, conf) + } + + // plain non-encrypted connection + return net.Dial("unix", "/run/osbuild-composer/job.socket") + +} + +func NewClient(remoteAddress string) *ComposerClient { client := &http.Client{ Transport: &http.Transport{ DialContext: func(context context.Context, network, addr string) (net.Conn, error) { - return net.Dial("unix", "/run/osbuild-composer/job.socket") + return newConnection(remoteAddress) }, }, } - return &ComposerClient{client} + hostname := remoteAddress + + // in case of unix domain socket, use localhost as hostname + if hostname == "" { + hostname = "localhost" + } + + return &ComposerClient{client, hostname} } func (c *ComposerClient) AddJob() (*jobqueue.Job, error) { @@ -36,14 +101,16 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) { var b bytes.Buffer json.NewEncoder(&b).Encode(request{}) - response, err := c.client.Post("http://localhost/job-queue/v1/jobs", "application/json", &b) + response, err := c.client.Post(c.createURL("/job-queue/v1/jobs"), "application/json", &b) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusCreated { - return nil, errors.New("couldn't create job") + rawR, _ := ioutil.ReadAll(response.Body) + r := string(rawR) + return nil, errors.New(fmt.Sprintf("couldn't create job, got %d: %s", response.StatusCode, r)) } job := &jobqueue.Job{} @@ -58,7 +125,8 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) { func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status common.ImageBuildState, result *common.ComposeResult) error { var b bytes.Buffer json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, result}) - url := fmt.Sprintf("http://localhost/job-queue/v1/jobs/%s/builds/%d", job.ID.String(), job.ImageBuildID) + urlPath := fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d", job.ID.String(), job.ImageBuildID) + url := c.createURL(urlPath) req, err := http.NewRequest("PATCH", url, &b) if err != nil { return err @@ -86,6 +154,10 @@ func (c *ComposerClient) UploadImage(job *jobqueue.Job, reader io.Reader) error return err } +func (c *ComposerClient) createURL(path string) string { + return "http://" + c.hostname + path +} + func handleJob(client *ComposerClient) error { fmt.Println("Waiting for a new job...") job, err := client.AddJob() @@ -109,7 +181,11 @@ func handleJob(client *ComposerClient) error { } func main() { - client := NewClient() + var remoteAddress string + flag.StringVar(&remoteAddress, "remote", "", "Connect to a remote composer using the specified address") + flag.Parse() + + client := NewClient(remoteAddress) for { if err := handleJob(client); err != nil { log.Fatalf("Failed to handle job: " + err.Error()) diff --git a/distribution/osbuild-remote-worker.socket b/distribution/osbuild-remote-worker.socket new file mode 100644 index 000000000..6753ec3e9 --- /dev/null +++ b/distribution/osbuild-remote-worker.socket @@ -0,0 +1,9 @@ +[Unit] +Description=OSBuild Composer API sockets + +[Socket] +Service=osbuild-composer.service +ListenStream=8700 + +[Install] +WantedBy=sockets.target diff --git a/distribution/osbuild-remote-worker@.service b/distribution/osbuild-remote-worker@.service new file mode 100644 index 000000000..0b29910dd --- /dev/null +++ b/distribution/osbuild-remote-worker@.service @@ -0,0 +1,13 @@ +[Unit] +Description=OSBuild Composer Remote Worker (%i) +After=multi-user.target + +[Service] +Type=simple +PrivateTmp=true +ExecStart=/usr/libexec/osbuild-composer/osbuild-worker --remote %i +CacheDirectory=osbuild-composer +Restart=on-failure +RestartSec=10s +CPUSchedulingPolicy=batch +IOSchedulingClass=idle