Add tcp and tls support for worker and job API

There's a usecase for running workers at a different machine than
the composer. For example when there's need for making images for
architecture different then the composer is running at. Although osbuild has
some kind of support for cross-architecture builds, we still consider it
as experimental, not-yet-production-ready feature.

This commit adds a support to composer and worker to communicate using TCP.
To ensure safe communication through the wild worlds of Internet, TLS is not
only supported but even required when using TCP. Both server and client
TLS authentication are required. This means both sides must have their own
private key/certificate pair and both certificates must be signed using one
certificate authority. Examples how to generate all this fancy crypto stuff
can be found in Makefile.

Changes on the composer side:
When osbuild-remote-worker.socket is started before osbuild-composer.service,
osbuild-composer also serves jobqueue API on this socket. The unix domain
socket is not affected by this changes - it is enabled at all times
independently on the remote one. The osbuild-remote-worker.socket listens
by default on TCP port 8700.

When running the composer with remote worker socket enabled, the following
files are required:
- /etc/osbuild-composer/ca-crt.pem     (CA certificate)
- /etc/osbuild-composer/composer-key.pem (composer private key)
- /etc/osbuild-composer/composer-crt.pem (composer certificate)

Changes on the worker side:
osbuild-worker has now --remote argument taking the address to a composer
instance. When present, the worker will try to establish TLS secured TCP
connection with the composer. When not present, the worker will use
the unix domain socket method. The unit template file osbuild-remote-worker
was added to simplify the spawning of workers. For example

systemctl start osbuild-remote-worker@example.com

starts a worker which will attempt to connect to the composer instance
running on the address example.com.

When running the worker with --remote argument, the following files are
required:
- /etc/osbuild-composer/ca-crt.pem     (CA certificate)
- /etc/osbuild-composer/worker-key.pem (worker private key)
- /etc/osbuild-composer/worker-crt.pem (worker certificate)

By default osbuild-composer.service will always spawn one local worker.
If you don't want it you need to mask the default worker unit by:
systemctl mask osbuild-worker@1.service

Closing remarks:
Remember that both composer and worker certificate must be signed by
the same CA!
This commit is contained in:
Ondřej Budai 2020-01-22 11:08:20 +01:00 committed by Tom Gundersen
parent 412ce45667
commit 820d23fd9d
5 changed files with 192 additions and 14 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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())

View file

@ -0,0 +1,9 @@
[Unit]
Description=OSBuild Composer API sockets
[Socket]
Service=osbuild-composer.service
ListenStream=8700
[Install]
WantedBy=sockets.target

View file

@ -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