Repository: osbuild/osbuild-composer
Pull request #2695 (2022-05-26 14:28:15)

oscap: implement OpenSCAP build remediation
This pull request includes:
- blueprint customizations for openscap integration
- osbuild2 stage for oscap remediatoin
- validation of customization options
- add oscap stage to fedora, rhel87 and rhel91 non-rpmostree builds

Depends on:
https://github.com/osbuild/osbuild/pull/1033

- [x] adequate testing for the new functionality or fixed issue
- [x] adequate documentation informing people about the change such as
  - [x] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`

Information regarding our GitLab pipeline (Schutzbot):

CI will not be ran automatically if WIP label is applied or the PR is in DRAFT state, instead only
a link will be provided to the pipeline which can then be triggered manually if desired. To run the
CI automatically either switch the PR to ready or apply WIP+test label.

Outside contributors need manual approval from one of the osbuild-composer maintainers.

Schutzbot will only be triggered if all Tests jobs in GitHub workflow succeed.
-->


kingsleyzissou: Yeah that's a good point. I was wondering if it would be better to do as you suggested or just have a predefined set of options once you specified the security benchmark. I will iterate over this and try something like that out 
achilleas-k: Oh, one more thing.
Could you rebase on `main` now?  There aren't any conflicts, but since #2805 was merged, we might get some fun side effects with 8.4 in rhel8.
kingsleyzissou: Thanks for the reveiw @achilleas-k, I'll rebase (I intentionally left `8.4` alone for that reason) and address the comments.
achilleas-k: Did something change or was this just a rebase on `main`?
kingsleyzissou: There was a merge conflict. So I rebased. I also updated the fedora `aarch` repos because I suspect those were breaking some of the base tests.
kingsleyzissou: Following some feedback from a call with the OpenSCAP team I made some slight changes to the PR:
- field for the `datastream` in the blueprints (since there can be multiple per distro)
- Limited the tests to `RHEL 8` & `RHEL-9`

Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 48cb4d620..a8f13ae89 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -605,6 +605,20 @@ Multi-tenancy:
     RUNNER: aws/rhel-8.6-ga-x86_64
     INTERNAL_NETWORK: "true"
 
+OpenSCAP:
+  stage: test
+  extends: .terraform
+  rules:
+    - !reference [.upstream_rules, rules]
+  script:
+    - schutzbot/deploy.sh
+    - /usr/libexec/tests/osbuild-composer/oscap.sh
+  parallel:
+    matrix:
+      - RUNNER:
+          - rhos-01/rhel-8.7-nightly-x86_64-large
+          - rhos-01/rhel-9.1-nightly-x86_64-large
+
 Upgrade:
   stage: test
   extends: .terraform/openstack
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index 552383147..d5e6c94c5 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -128,6 +128,10 @@ func setProtection(protected bool) {
 		}
 		return
 	}
+	if len(asInstanceOutput.AutoScalingInstances) == 0 {
+		logrus.Info("No Autoscaling instace is defined")
+		return
+	}
 
 	// make the request to protect (or unprotect) the instance
 	input := &autoscaling.SetInstanceProtectionInput{
diff --git a/distribution/Dockerfile-config b/distribution/Dockerfile-config
index 20d2a9834..8ea6c5864 100644
--- a/distribution/Dockerfile-config
+++ b/distribution/Dockerfile-config
@@ -5,7 +5,7 @@ RUN dnf -y install \
 
 COPY ./tools/gen-certs.sh .
 COPY ./test/data/x509/openssl.cnf .
-COPY ./test/data/composer/osbuild-composer.toml /config/
-COPY ./test/data/worker/osbuild-worker.toml /config/
+COPY ./test/data/composer/osbuild-composer-tls.toml /config/
+COPY ./test/data/worker/osbuild-worker-tls.toml /config/
 COPY ./repositories /config/repositories/
 RUN ./gen-certs.sh ./openssl.cnf /config  /config/ca
diff --git a/internal/blueprint/customizations.go b/internal/blueprint/customizations.go
index fee02ebc2..69b0db2e2 100644
--- a/internal/blueprint/customizations.go
+++ b/internal/blueprint/customizations.go
@@ -22,6 +22,7 @@ type Customizations struct {
 	Filesystem         []FilesystemCustomization `json:"filesystem,omitempty" toml:"filesystem,omitempty"`
 	InstallationDevice string                    `json:"installation_device,omitempty" toml:"installation_device,omitempty"`
 	FDO                *FDOCustomization         `json:"fdo,omitempty" toml:"fdo,omitempty"`
+	OpenSCAP           *OpenSCAPCustomization    `json:"openscap,omitempty" toml:"openscap,omitempty"`
 }
 
 type FDOCustomization struct {
@@ -90,6 +91,11 @@ type FilesystemCustomization struct {
 	MinSize    uint64 `json:"minsize,omitempty" toml:"size,omitempty"`
 }
 
+type OpenSCAPCustomization struct {
+	DataStream string `json:"datastream,omitempty" toml:"datastream,omitempty"`
+	ProfileID  string `json:"profile_id,omitempty" toml:"profile_id,omitempty"`
+}
+
 func (fsc *FilesystemCustomization) UnmarshalTOML(data interface{}) error {
 	d, _ := data.(map[string]interface{})
 
@@ -367,3 +373,10 @@ func (c *Customizations) GetFDO() *FDOCustomization {
 	}
 	return c.FDO
 }
+
+func (c *Customizations) GetOpenSCAP() *OpenSCAPCustomization {
+	if c == nil {
+		return nil
+	}
+	return c.OpenSCAP
+}
diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go
index ba878406b..9a9ecbcac 100644
--- a/internal/cloudapi/v2/handler.go
+++ b/internal/cloudapi/v2/handler.go
@@ -173,6 +173,19 @@ func (h *apiHandlers) PostCompose(ctx echo.Context) error {
 		}
 	}
 
+	if request.Customizations != nil && request.Customizations.Containers != nil {
+		for _, c := range *request.Customizations.Containers {
+			bc := blueprint.Container{
+				Source:    c.Source,
+				TLSVerify: c.TlsVerify,
+			}
+			if c.Name != nil {
+				bc.Name = *c.Name
+			}
+			bp.Containers = append(bp.Containers, bc)
+		}
+	}
+
 	if request.Customizations != nil && request.Customizations.Filesystem != nil {
 		var fsCustomizations []blueprint.FilesystemCustomization
 		for _, f := range *request.Customizations.Filesystem {
diff --git a/internal/cloudapi/v2/openapi.v2.gen.go b/internal/cloudapi/v2/openapi.v2.gen.go
index bd43ca82a..91cd5ae99 100644
--- a/internal/cloudapi/v2/openapi.v2.gen.go
+++ b/internal/cloudapi/v2/openapi.v2.gen.go
@@ -220,6 +220,16 @@ type ComposeStatusError struct {
 // ComposeStatusValue defines model for ComposeStatusValue.
 type ComposeStatusValue string
 
+// Container defines model for Container.
+type Container struct {
+	// Name to use for the container from the image
+	Name   *string `json:"name,omitempty"`
+	Source string  `json:"source"`
+
+	// Control TLS verifification
+	TlsVerify *bool `json:"tls_verify,omitempty"`
+}
+
 // ContainerUploadOptions defines model for ContainerUploadOptions.
 type ContainerUploadOptions struct {
 	// Name for the created container image
@@ -240,6 +250,7 @@ type ContainerUploadStatus struct {
 
 // Customizations defines model for Customizations.
 type Customizations struct {
+	Containers *[]Container  `json:"containers,omitempty"`
 	Filesystem *[]Filesystem `json:"filesystem,omitempty"`
 	Packages   *[]string     `json:"packages,omitempty"`
 
@@ -692,112 +703,114 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"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",
+	"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",
+	"R6B2l8FfAAA=",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/internal/cloudapi/v2/openapi.v2.yml b/internal/cloudapi/v2/openapi.v2.yml
index 3d49c14cd..7229bd784 100644
--- a/internal/cloudapi/v2/openapi.v2.yml
+++ b/internal/cloudapi/v2/openapi.v2.yml
@@ -859,6 +859,11 @@ components:
     Customizations:
       type: object
       properties:
+        containers:
+          type: array
+          items:
+             $ref: '#/components/schemas/Container'
+             description: Container images to embed into the final artfact
         subscription:
           $ref: '#/components/schemas/Subscription'
         packages:
@@ -902,6 +907,22 @@ components:
               items:
                 type: string
                 example: "firewalld"
+    Container:
+      type: object
+      required:
+        - source
+      properties:
+        source:
+          type: string
+          desription: Reference to the container to embed
+          example: 'registry.example.com/image:tag'
+        name:
+          type: string
+          description: Name to use for the container from the image
+        tls_verify:
+          type: boolean
+          description: Control TLS verifification
+          example: true
     Filesystem:
       type: object
       required:
diff --git a/internal/cloudapi/v2/server.go b/internal/cloudapi/v2/server.go
index a1acb9bb4..5a01977b7 100644
--- a/internal/cloudapi/v2/server.go
+++ b/internal/cloudapi/v2/server.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/container"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/distroregistry"
 	"github.com/osbuild/osbuild-composer/internal/prometheus"
@@ -112,7 +113,33 @@ func (s *Server) enqueueCompose(distribution distro.Distro, bp blueprint.Bluepri
 		return id, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
 	}
 
-	manifestJobID, err := s.workers.EnqueueManifestJobByID(&worker.ManifestJobByID{}, depsolveJobID, channel)
+	dependencies := []uuid.UUID{depsolveJobID}
+	var containerResolveJob uuid.UUID
+	if len(bp.Containers) > 0 {
+		job := worker.ContainerResolveJob{
+			Arch:  ir.arch.Name(),
+			Specs: make([]worker.ContainerSpec, len(bp.Containers)),
+		}
+
+		for i, c := range bp.Containers {
+			job.Specs[i] = worker.ContainerSpec{
+				Source:    c.Source,
+				Name:      c.Name,
+				TLSVerify: c.TLSVerify,
+			}
+		}
+
+		jobId, err := s.workers.EnqueueContainerResolveJob(&job, channel)
+
+		if err != nil {
+			return id, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
+		}
+
+		containerResolveJob = jobId
+		dependencies = append(dependencies, jobId)
+	}
+
+	manifestJobID, err := s.workers.EnqueueManifestJobByID(&worker.ManifestJobByID{}, dependencies, channel)
 	if err != nil {
 		return id, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
 	}
@@ -130,7 +157,7 @@ func (s *Server) enqueueCompose(distribution distro.Distro, bp blueprint.Bluepri
 
 	s.goroutinesGroup.Add(1)
 	go func() {
-		generateManifest(s.goroutinesCtx, s.workers, depsolveJobID, manifestJobID, ir.imageType, ir.repositories, ir.imageOptions, manifestSeed, bp.Customizations)
+		generateManifest(s.goroutinesCtx, s.workers, depsolveJobID, containerResolveJob, manifestJobID, ir.imageType, ir.repositories, ir.imageOptions, manifestSeed, bp.Customizations)
 		defer s.goroutinesGroup.Done()
 	}()
 
@@ -164,7 +191,33 @@ func (s *Server) enqueueKojiCompose(taskID uint64, server, name, version, releas
 			return id, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
 		}
 
-		manifestJobID, err := s.workers.EnqueueManifestJobByID(&worker.ManifestJobByID{}, depsolveJobID, channel)
+		var containerResolveJob uuid.UUID
+		dependencies := []uuid.UUID{depsolveJobID}
+		if len(bp.Containers) > 0 {
+			job := worker.ContainerResolveJob{
+				Arch:  ir.arch.Name(),
+				Specs: make([]worker.ContainerSpec, len(bp.Containers)),
+			}
+
+			for i, c := range bp.Containers {
+				job.Specs[i] = worker.ContainerSpec{
+					Source:    c.Source,
+					Name:      c.Name,
+					TLSVerify: c.TLSVerify,
+				}
+			}
+
+			jobId, err := s.workers.EnqueueContainerResolveJob(&job, channel)
+
+			if err != nil {
+				return id, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
+			}
+
+			containerResolveJob = jobId
+			dependencies = append(dependencies, jobId)
+		}
+
+		manifestJobID, err := s.workers.EnqueueManifestJobByID(&worker.ManifestJobByID{}, dependencies, channel)
 		if err != nil {
 			return id, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
 		}
@@ -208,7 +261,7 @@ func (s *Server) enqueueKojiCompose(taskID uint64, server, name, version, releas
 		// copy the image request while passing it into the goroutine to prevent data races
 		s.goroutinesGroup.Add(1)
 		go func(ir imageRequest) {
-			generateManifest(s.goroutinesCtx, s.workers, depsolveJobID, manifestJobID, ir.imageType, ir.repositories, ir.imageOptions, manifestSeed, bp.Customizations)
+			generateManifest(s.goroutinesCtx, s.workers, depsolveJobID, containerResolveJob, manifestJobID, ir.imageType, ir.repositories, ir.imageOptions, manifestSeed, bp.Customizations)
 			defer s.goroutinesGroup.Done()
 		}(ir)
 	}
@@ -229,7 +282,7 @@ func (s *Server) enqueueKojiCompose(taskID uint64, server, name, version, releas
 	return id, nil
 }
 
-func generateManifest(ctx context.Context, workers *worker.Server, depsolveJobID uuid.UUID, manifestJobID uuid.UUID, imageType distro.ImageType, repos []rpmmd.RepoConfig, options distro.ImageOptions, seed int64, b *blueprint.Customizations) {
+func generateManifest(ctx context.Context, workers *worker.Server, depsolveJobID, containerResolveJobID, manifestJobID uuid.UUID, imageType distro.ImageType, repos []rpmmd.RepoConfig, options distro.ImageOptions, seed int64, b *blueprint.Customizations) {
 	ctx, cancel := context.WithTimeout(ctx, time.Minute*5)
 	defer cancel()
 
@@ -313,7 +366,36 @@ func generateManifest(ctx context.Context, workers *worker.Server, depsolveJobID
 		return
 	}
 
-	manifest, err := imageType.Manifest(b, options, repos, depsolveResults.PackageSpecs, nil, seed)
+	var containerSpecs []container.Spec
+	if len(dynArgs) == 2 {
+		// Container resolve job
+		var result worker.ContainerResolveJobResult
+
+		_, err := workers.ContainerResolveJobInfo(containerResolveJobID, &result)
+
+		if err != nil {
+			reason := "Error reading container resolve job status"
+			jobResult.JobError = clienterrors.WorkerClientError(clienterrors.ErrorReadingJobStatus, reason, nil)
+			return
+		}
+
+		if jobErr := result.JobError; jobErr != nil {
+			jobResult.JobError = clienterrors.WorkerClientError(clienterrors.ErrorContainerDependency, "Error in container resolve job dependency", nil)
+			return
+		}
+
+		containerSpecs = make([]container.Spec, len(result.Specs))
+
+		for i, s := range result.Specs {
+			containerSpecs[i].Source = s.Source
+			containerSpecs[i].Digest = s.Digest
+			containerSpecs[i].LocalName = s.Name
+			containerSpecs[i].TLSVerify = s.TLSVerify
+			containerSpecs[i].ImageID = s.ImageID
+		}
+	}
+
+	manifest, err := imageType.Manifest(b, options, repos, depsolveResults.PackageSpecs, containerSpecs, seed)
 	if err != nil {
 		reason := "Error generating manifest"
 		jobResult.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestGeneration, reason, nil)
diff --git a/internal/distro/fedora/distro.go b/internal/distro/fedora/distro.go
index 3d739b6e6..56affff1d 100644
--- a/internal/distro/fedora/distro.go
+++ b/internal/distro/fedora/distro.go
@@ -14,6 +14,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/environment"
 	"github.com/osbuild/osbuild-composer/internal/image"
 	"github.com/osbuild/osbuild-composer/internal/manifest"
+	"github.com/osbuild/osbuild-composer/internal/oscap"
 	"github.com/osbuild/osbuild-composer/internal/platform"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/runner"
@@ -49,6 +50,12 @@ var (
 		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
 	}
 
+	oscapProfileAllowList = []oscap.Profile{
+		oscap.Ospp,
+		oscap.PciDss,
+		oscap.Standard,
+	}
+
 	// Services
 	iotServices = []string{
 		"NetworkManager.service",
@@ -704,6 +711,22 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
 	}
 
+	if osc := customizations.GetOpenSCAP(); osc != nil {
+		supported := oscap.IsProfileAllowed(osc.ProfileID, oscapProfileAllowList)
+		if !supported {
+			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
+		}
+		if t.rpmOstree {
+			return fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
+		}
+		if osc.DataStream == "" {
+			return fmt.Errorf("OpenSCAP datastream cannot be empty")
+		}
+		if osc.ProfileID == "" {
+			return fmt.Errorf("OpenSCAP profile cannot be empty")
+		}
+	}
+
 	return nil
 }
 
diff --git a/internal/distro/fedora/images.go b/internal/distro/fedora/images.go
index 63c9b2c84..5cdaf777d 100644
--- a/internal/distro/fedora/images.go
+++ b/internal/distro/fedora/images.go
@@ -7,6 +7,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/image"
 	"github.com/osbuild/osbuild-composer/internal/manifest"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/workload"
 )
@@ -90,6 +91,18 @@ func osCustomizations(
 		osc.SElinux = "targeted"
 	}
 
+	if oscapConfig := c.GetOpenSCAP(); oscapConfig != nil {
+		if t.rpmOstree {
+			panic("unexpected oscap options for ostree image type")
+		}
+		osc.OpenSCAPConfig = osbuild.NewOscapRemediationStageOptions(
+			osbuild.OscapConfig{
+				Datastream: oscapConfig.DataStream,
+				ProfileID:  oscapConfig.ProfileID,
+			},
+		)
+	}
+
 	osc.Grub2Config = imageConfig.Grub2Config
 	osc.Sysconfig = imageConfig.Sysconfig
 	osc.SystemdLogind = imageConfig.SystemdLogind
diff --git a/internal/distro/rhel7/distro.go b/internal/distro/rhel7/distro.go
index 17ded1f02..00726e639 100644
--- a/internal/distro/rhel7/distro.go
+++ b/internal/distro/rhel7/distro.go
@@ -448,6 +448,10 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
 	}
 
+	if osc := customizations.GetOpenSCAP(); osc != nil {
+		return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
+	}
+
 	return nil
 }
 
diff --git a/internal/distro/rhel8/distro.go b/internal/distro/rhel8/distro.go
index 39971dd4d..a2d54ef07 100644
--- a/internal/distro/rhel8/distro.go
+++ b/internal/distro/rhel8/distro.go
@@ -14,6 +14,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/disk"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
+	"github.com/osbuild/osbuild-composer/internal/oscap"
 	"github.com/osbuild/osbuild-composer/internal/ostree"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -67,9 +68,31 @@ const (
 	blueprintPkgsKey = "blueprint"
 )
 
-var mountpointAllowList = []string{
-	"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-}
+var (
+	mountpointAllowList = []string{
+		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
+	}
+
+	// rhel8 allow all
+	oscapProfileAllowList = []oscap.Profile{
+		oscap.AnssiBp28Enhanced,
+		oscap.AnssiBp28High,
+		oscap.AnssiBp28Intermediary,
+		oscap.AnssiBp28Minimal,
+		oscap.Cis,
+		oscap.CisServerL1,
+		oscap.CisWorkstationL1,
+		oscap.CisWorkstationL2,
+		oscap.Cui,
+		oscap.E8,
+		oscap.Hippa,
+		oscap.IsmO,
+		oscap.Ospp,
+		oscap.PciDss,
+		oscap.Stig,
+		oscap.StigGui,
+	}
+)
 
 type distribution struct {
 	name               string
@@ -431,6 +454,15 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 		mergedSets[buildPkgsKey] = mergedSets[buildPkgsKey].Append(extraPkgs)
 	}
 
+	// if oscap customizations are enabled we need to add `openscap-scanner`
+	// and `scap-security-guides` packages to build root
+	if bp.Customizations.GetOpenSCAP() != nil {
+		mergedSets[buildPkgsKey] = mergedSets[buildPkgsKey].Append(rpmmd.PackageSet{Include: []string{
+			"openscap-scanner",
+			"scap-security-guide",
+		}})
+	}
+
 	// depsolve bp packages separately
 	// bp packages aren't restricted by exclude lists
 	mergedSets[blueprintPkgsKey] = rpmmd.PackageSet{Include: bpPackages}
@@ -646,6 +678,26 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
 	}
 
+	if osc := customizations.GetOpenSCAP(); osc != nil {
+		// only add support for RHEL 8.7 and above. centos not supported.
+		if !t.arch.distro.isRHEL() || versionLessThan(t.arch.distro.osVersion, "8.7") {
+			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
+		}
+		supported := oscap.IsProfileAllowed(osc.ProfileID, oscapProfileAllowList)
+		if !supported {
+			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
+		}
+		if t.rpmOstree {
+			return fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
+		}
+		if osc.DataStream == "" {
+			return fmt.Errorf("OpenSCAP datastream cannot be empty")
+		}
+		if osc.ProfileID == "" {
+			return fmt.Errorf("OpenSCAP profile cannot be empty")
+		}
+	}
+
 	return nil
 }
 
diff --git a/internal/distro/rhel8/pipelines.go b/internal/distro/rhel8/pipelines.go
index 7939d335e..33f4874b5 100644
--- a/internal/distro/rhel8/pipelines.go
+++ b/internal/distro/rhel8/pipelines.go
@@ -673,6 +673,19 @@ func osPipeline(t *imageType,
 		p.AddStage(bootloader)
 	}
 
+	if oscapConfig := c.GetOpenSCAP(); oscapConfig != nil {
+		if t.rpmOstree {
+			return nil, fmt.Errorf("unexpected oscap options for ostree image type")
+		}
+		remediationOptions := osbuild.NewOscapRemediationStageOptions(
+			osbuild.OscapConfig{
+				Datastream: oscapConfig.DataStream,
+				ProfileID:  oscapConfig.ProfileID,
+			},
+		)
+		p.AddStage(osbuild.NewOscapRemediationStage(remediationOptions))
+	}
+
 	if !imageConfig.NoSElinux {
 		p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	}
diff --git a/internal/distro/rhel90/distro.go b/internal/distro/rhel90/distro.go
index f4c60efb3..6880e719d 100644
--- a/internal/distro/rhel90/distro.go
+++ b/internal/distro/rhel90/distro.go
@@ -14,6 +14,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/disk"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
+	"github.com/osbuild/osbuild-composer/internal/oscap"
 	"github.com/osbuild/osbuild-composer/internal/ostree"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -37,9 +38,33 @@ const (
 	blueprintPkgsKey = "blueprint"
 )
 
-var mountpointAllowList = []string{
-	"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
-}
+var (
+	mountpointAllowList = []string{
+		"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
+	}
+
+	// rhel9 & cs9 share the same list
+	// of allowed profiles so a single
+	// allow list can be used
+	oscapProfileAllowList = []oscap.Profile{
+		oscap.AnssiBp28Enhanced,
+		oscap.AnssiBp28High,
+		oscap.AnssiBp28Intermediary,
+		oscap.AnssiBp28Minimal,
+		oscap.Cis,
+		oscap.CisServerL1,
+		oscap.CisWorkstationL1,
+		oscap.CisWorkstationL2,
+		oscap.Cui,
+		oscap.E8,
+		oscap.Hippa,
+		oscap.IsmO,
+		oscap.Ospp,
+		oscap.PciDss,
+		oscap.Stig,
+		oscap.StigGui,
+	}
+)
 
 type distribution struct {
 	name               string
@@ -365,6 +390,15 @@ func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOpti
 		mergedSets[buildPkgsKey] = mergedSets[buildPkgsKey].Append(extraPkgs)
 	}
 
+	// if oscap customizations are enabled we need to add `openscap-scanner`
+	// and `scap-security-guides` packages to build root
+	if bp.Customizations.GetOpenSCAP() != nil {
+		mergedSets[buildPkgsKey] = mergedSets[buildPkgsKey].Append(rpmmd.PackageSet{Include: []string{
+			"openscap-scanner",
+			"scap-security-guide",
+		}})
+	}
+
 	// depsolve bp packages separately
 	// bp packages aren't restricted by exclude lists
 	mergedSets[blueprintPkgsKey] = rpmmd.PackageSet{Include: bpPackages}
@@ -580,6 +614,24 @@ func (t *imageType) checkOptions(customizations *blueprint.Customizations, optio
 		return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
 	}
 
+	if osc := customizations.GetOpenSCAP(); osc != nil {
+		if t.arch.distro.osVersion == "9.0" {
+			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
+		}
+		if !oscap.IsProfileAllowed(osc.ProfileID, oscapProfileAllowList) {
+			return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported profile: %s", osc.ProfileID))
+		}
+		if t.rpmOstree {
+			return fmt.Errorf("OpenSCAP customizations are not supported for ostree types")
+		}
+		if osc.DataStream == "" {
+			return fmt.Errorf("OpenSCAP datastream cannot be empty")
+		}
+		if osc.ProfileID == "" {
+			return fmt.Errorf("OpenSCAP profile cannot be empty")
+		}
+	}
+
 	return nil
 }
 
diff --git a/internal/distro/rhel90/pipelines.go b/internal/distro/rhel90/pipelines.go
index 5c899bd1a..f0a910806 100644
--- a/internal/distro/rhel90/pipelines.go
+++ b/internal/distro/rhel90/pipelines.go
@@ -671,6 +671,19 @@ func osPipeline(t *imageType,
 		p.AddStage(bootloader)
 	}
 
+	if oscapConfig := c.GetOpenSCAP(); oscapConfig != nil {
+		if t.rpmOstree {
+			return nil, fmt.Errorf("unexpected oscap options for ostree image type")
+		}
+		remediationOptions := osbuild.NewOscapRemediationStageOptions(
+			osbuild.OscapConfig{
+				Datastream: oscapConfig.DataStream,
+				ProfileID:  oscapConfig.ProfileID,
+			},
+		)
+		p.AddStage(osbuild.NewOscapRemediationStage(remediationOptions))
+	}
+
 	if !imageConfig.NoSElinux {
 		p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	}
diff --git a/internal/manifest/os.go b/internal/manifest/os.go
index 8f294c872..0642c83f0 100644
--- a/internal/manifest/os.go
+++ b/internal/manifest/os.go
@@ -70,23 +70,24 @@ type OSCustomizations struct {
 	Users    []blueprint.UserCustomization
 	Firewall *blueprint.FirewallCustomization
 	// TODO: drop osbuild types from the API
-	Grub2Config   *osbuild.GRUB2Config
-	Sysconfig     []*osbuild.SysconfigStageOptions
-	SystemdLogind []*osbuild.SystemdLogindStageOptions
-	CloudInit     []*osbuild.CloudInitStageOptions
-	Modprobe      []*osbuild.ModprobeStageOptions
-	DracutConf    []*osbuild.DracutConfStageOptions
-	SystemdUnit   []*osbuild.SystemdUnitStageOptions
-	Authselect    *osbuild.AuthselectStageOptions
-	SELinuxConfig *osbuild.SELinuxConfigStageOptions
-	Tuned         *osbuild.TunedStageOptions
-	Tmpfilesd     []*osbuild.TmpfilesdStageOptions
-	PamLimitsConf []*osbuild.PamLimitsConfStageOptions
-	Sysctld       []*osbuild.SysctldStageOptions
-	DNFConfig     []*osbuild.DNFConfigStageOptions
-	SshdConfig    *osbuild.SshdConfigStageOptions
-	AuthConfig    *osbuild.AuthconfigStageOptions
-	PwQuality     *osbuild.PwqualityConfStageOptions
+	Grub2Config    *osbuild.GRUB2Config
+	Sysconfig      []*osbuild.SysconfigStageOptions
+	SystemdLogind  []*osbuild.SystemdLogindStageOptions
+	CloudInit      []*osbuild.CloudInitStageOptions
+	Modprobe       []*osbuild.ModprobeStageOptions
+	DracutConf     []*osbuild.DracutConfStageOptions
+	SystemdUnit    []*osbuild.SystemdUnitStageOptions
+	Authselect     *osbuild.AuthselectStageOptions
+	SELinuxConfig  *osbuild.SELinuxConfigStageOptions
+	Tuned          *osbuild.TunedStageOptions
+	Tmpfilesd      []*osbuild.TmpfilesdStageOptions
+	PamLimitsConf  []*osbuild.PamLimitsConfStageOptions
+	Sysctld        []*osbuild.SysctldStageOptions
+	DNFConfig      []*osbuild.DNFConfigStageOptions
+	SshdConfig     *osbuild.SshdConfigStageOptions
+	AuthConfig     *osbuild.AuthconfigStageOptions
+	PwQuality      *osbuild.PwqualityConfStageOptions
+	OpenSCAPConfig *osbuild.OscapRemediationStageOptions
 }
 
 // OS represents the filesystem tree of the target image. This roughly
@@ -185,6 +186,9 @@ func (p *OS) getBuildPackages() []string {
 		packages = append(packages, "policycoreutils")
 		packages = append(packages, fmt.Sprintf("selinux-policy-%s", p.SElinux))
 	}
+	if p.OpenSCAPConfig != nil {
+		packages = append(packages, "openscap-scanner", "scap-security-guide")
+	}
 	return packages
 }
 
@@ -418,6 +422,10 @@ func (p *OS) serialize() osbuild.Pipeline {
 		pipeline.AddStage(bootloader)
 	}
 
+	if p.OpenSCAPConfig != nil {
+		pipeline.AddStage(osbuild.NewOscapRemediationStage(p.OpenSCAPConfig))
+	}
+
 	if p.SElinux != "" {
 		pipeline.AddStage(osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
 			FileContexts: fmt.Sprintf("etc/selinux/%s/contexts/files/file_contexts", p.SElinux),
diff --git a/internal/osbuild/oscap_remediation_stage.go b/internal/osbuild/oscap_remediation_stage.go
new file mode 100644
index 000000000..04d5a57a6
--- /dev/null
+++ b/internal/osbuild/oscap_remediation_stage.go
@@ -0,0 +1,87 @@
+package osbuild
+
+import "fmt"
+
+type OscapVerbosityLevel string
+
+const (
+	OscapVerbosityLevelDevel   = "DEVEL"
+	OscapVerbosityLevelInfo    = "INFO"
+	OscapVerbosityLevelError   = "ERROR"
+	OscapVerbosityLevelWarning = "WARNING"
+)
+
+type OscapRemediationStageOptions struct {
+	DataDir string      `json:"data_dir,omitempty"`
+	Config  OscapConfig `json:"config"`
+}
+type OscapConfig struct {
+	Datastream   string              `json:"datastream" toml:"datastream"`
+	ProfileID    string              `json:"profile_id" toml:"profile_id"`
+	DatastreamID string              `json:"datastream_id,omitempty" toml:"datastream_id,omitempty"`
+	XCCDFID      string              `json:"xccdf_id,omitempty" toml:"xccdf_id,omitempty"`
+	BenchmarkID  string              `json:"benchmark_id,omitempty" toml:"benchmark_id,omitempty"`
+	Tailoring    string              `json:"tailoring,omitempty" toml:"tailoring,omitempty"`
+	TailoringID  string              `json:"tailoring_id,omitempty" toml:"tailoring_id,omitempty"`
+	ArfResult    string              `json:"arf_result,omitempty" toml:"arf_result,omitempty"`
+	HtmlReport   string              `json:"html_report,omitempty" toml:"html_report,omitempty"`
+	VerboseLog   string              `json:"verbose_log,omitempty" toml:"verbose_log,omitempty"`
+	VerboseLevel OscapVerbosityLevel `json:"verbose_level,omitempty" toml:"verbose_level,omitempty"`
+}
+
+func (OscapRemediationStageOptions) isStageOptions() {}
+
+func (c OscapConfig) validate() error {
+	if c.Datastream == "" {
+		return fmt.Errorf("'datastream' must be specified")
+	}
+	if c.ProfileID == "" {
+		return fmt.Errorf("'profile_id' must be specified")
+	}
+	if c.VerboseLevel != "" {
+		allowedVerboseLevelValues := []OscapVerbosityLevel{
+			OscapVerbosityLevelDevel,
+			OscapVerbosityLevelError,
+			OscapVerbosityLevelInfo,
+			OscapVerbosityLevelWarning,
+		}
+		valid := false
+		for _, value := range allowedVerboseLevelValues {
+			if c.VerboseLevel == value {
+				valid = true
+				break
+			}
+		}
+		if !valid {
+			return fmt.Errorf("'verbose_level' option does not allow %q as a value", c.VerboseLevel)
+		}
+	}
+	return nil
+}
+
+func NewOscapRemediationStage(options *OscapRemediationStageOptions) *Stage {
+	if err := options.Config.validate(); err != nil {
+		panic(err)
+	}
+
+	return &Stage{
+		Type:    "org.osbuild.oscap.remediation",
+		Options: options,
+	}
+}
+
+func NewOscapRemediationStageOptions(options OscapConfig) *OscapRemediationStageOptions {
+	return &OscapRemediationStageOptions{
+		Config: OscapConfig{
+			ProfileID:    options.ProfileID,
+			Datastream:   options.Datastream,
+			DatastreamID: options.DatastreamID,
+			XCCDFID:      options.XCCDFID,
+			BenchmarkID:  options.BenchmarkID,
+			ArfResult:    options.ArfResult,
+			HtmlReport:   options.HtmlReport,
+			VerboseLog:   options.VerboseLog,
+			VerboseLevel: options.VerboseLevel,
+		},
+	}
+}
diff --git a/internal/oscap/oscap.go b/internal/oscap/oscap.go
new file mode 100644
index 000000000..32fbc38df
--- /dev/null
+++ b/internal/oscap/oscap.go
@@ -0,0 +1,45 @@
+package oscap
+
+import "strings"
+
+type Profile string
+
+func (p Profile) String() string {
+	return string(p)
+}
+
+const (
+	AnssiBp28Enhanced     Profile = "xccdf_org.ssgproject.content_profile_anssi_bp28_enhanced"
+	AnssiBp28High         Profile = "xccdf_org.ssgproject.content_profile_anssi_bp28_high"
+	AnssiBp28Intermediary Profile = "xccdf_org.ssgproject.content_profile_anssi_bp28_intermediary"
+	AnssiBp28Minimal      Profile = "xccdf_org.ssgproject.content_profile_anssi_bp28_minimal"
+	Cis                   Profile = "xccdf_org.ssgproject.content_profile_cis"
+	CisServerL1           Profile = "xccdf_org.ssgproject.content_profile_cis_server_l1"
+	CisWorkstationL1      Profile = "xccdf_org.ssgproject.content_profile_cis_workstation_l1"
+	CisWorkstationL2      Profile = "xccdf_org.ssgproject.content_profile_cis_workstation_l2"
+	Cui                   Profile = "xccdf_org.ssgproject.content_profile_cui"
+	E8                    Profile = "xccdf_org.ssgproject.content_profile_e8"
+	Hippa                 Profile = "xccdf_org.ssgproject.content_profile_hipaa"
+	IsmO                  Profile = "xccdf_org.ssgproject.content_profile_ism_o"
+	Ospp                  Profile = "xccdf_org.ssgproject.content_profile_ospp"
+	PciDss                Profile = "xccdf_org.ssgproject.content_profile_pci-dss"
+	Standard              Profile = "xccdf_org.ssgproject.content_profile_standard"
+	Stig                  Profile = "xccdf_org.ssgproject.content_profile_stig"
+	StigGui               Profile = "xccdf_org.ssgproject.content_profile_stig_gui"
+)
+
+func IsProfileAllowed(profile string, allowlist []Profile) bool {
+	for _, a := range allowlist {
+		if a.String() == profile {
+			return true
+		}
+		// this enables a user to specify
+		// the full profile or the short
+		// profile id
+		if strings.HasSuffix(a.String(), profile) {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/internal/worker/clienterrors/errors.go b/internal/worker/clienterrors/errors.go
index 9e6003b51..c7a56d100 100644
--- a/internal/worker/clienterrors/errors.go
+++ b/internal/worker/clienterrors/errors.go
@@ -31,6 +31,7 @@ const (
 	ErrorTargetError          ClientErrorCode = 28
 	ErrorParsingJobArgs       ClientErrorCode = 29
 	ErrorContainerResolution  ClientErrorCode = 30
+	ErrorContainerDependency  ClientErrorCode = 31
 )
 
 type ClientErrorCode int
@@ -94,6 +95,8 @@ func GetStatusCode(err *Error) StatusCode {
 // IsDependencyError returns true if the error means that a dependency of a job failed
 func (e *Error) IsDependencyError() bool {
 	switch e.ID {
+	case ErrorContainerDependency:
+		return true
 	case ErrorDepsolveDependency:
 		return true
 	case ErrorManifestDependency:
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 20c0e76e7..1289a0681 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -146,8 +146,11 @@ func (s *Server) EnqueueDepsolve(job *DepsolveJob, channel string) (uuid.UUID, e
 	return s.enqueue(JobTypeDepsolve, job, nil, channel)
 }
 
-func (s *Server) EnqueueManifestJobByID(job *ManifestJobByID, parent uuid.UUID, channel string) (uuid.UUID, error) {
-	return s.enqueue(JobTypeManifestIDOnly, job, []uuid.UUID{parent}, channel)
+func (s *Server) EnqueueManifestJobByID(job *ManifestJobByID, dependencies []uuid.UUID, channel string) (uuid.UUID, error) {
+	if len(dependencies) == 0 {
+		panic("EnqueueManifestJobByID has no dependencies, expected at least a depsolve job")
+	}
+	return s.enqueue(JobTypeManifestIDOnly, job, dependencies, channel)
 }
 
 func (s *Server) EnqueueContainerResolveJob(job *ContainerResolveJob, channel string) (uuid.UUID, error) {
@@ -622,6 +625,14 @@ func (s *Server) FinishJob(token uuid.UUID, result json.RawMessage) error {
 		}
 		jobResult = &kojiFinalizeJR.JobResult
 
+	case JobTypeContainerResolve:
+		var containerResolveJR ContainerResolveJobResult
+		jobInfo, err = s.ContainerResolveJobInfo(jobId, &containerResolveJR)
+		if err != nil {
+			return err
+		}
+		jobResult = &containerResolveJR.JobResult
+
 	default:
 		return fmt.Errorf("unexpected job type: %s", jobType)
 	}
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 4b018c938..7e32c4194 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -205,6 +205,7 @@ install -m 0755 -vp tools/libvirt_test.sh                          %{buildroot}%
 install -m 0755 -vp tools/s3_test.sh                               %{buildroot}%{_libexecdir}/osbuild-composer-test/
 install -m 0755 -vp tools/generic_s3_test.sh                       %{buildroot}%{_libexecdir}/osbuild-composer-test/
 install -m 0755 -vp tools/generic_s3_https_test.sh                 %{buildroot}%{_libexecdir}/osbuild-composer-test/
+install -m 0755 -vp tools/run-mock-auth-servers.sh                 %{buildroot}%{_libexecdir}/osbuild-composer-test/
 install -m 0755 -vp tools/set-env-variables.sh                     %{buildroot}%{_libexecdir}/osbuild-composer-test/
 install -m 0755 -vp tools/test-case-generators/generate-test-cases %{buildroot}%{_libexecdir}/osbuild-composer-test/
 install -m 0755 -vd                                                %{buildroot}%{_libexecdir}/tests/osbuild-composer
diff --git a/schutzbot/team_ssh_keys.txt b/schutzbot/team_ssh_keys.txt
index 0f885eda0..8de500804 100644
--- a/schutzbot/team_ssh_keys.txt
+++ b/schutzbot/team_ssh_keys.txt
@@ -15,3 +15,4 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPqEtsCdSozq0DT8sOazpizsBP65Ni6SMqrQA85Wnfs1
 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod yih-redhat@redhat.com
 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYB3SyAYj+V/kmAt594RlpZlXRvVJ2r8+G1Jgnr6ft8Y6vpNkWZxpTVWEJicLczGYpzvq2AjkNStigU9Q1M2F21Te3SzT2kgNVXsMTqou4X//ZX20zej3gyI+25mc4LdBWxFaLsyrFqD76Fro2rAuCoylrfeIQBvFWbilrR+cAV9tFrJT9I4RWYVL8v7EUtBeXarVFIjwcCALzLHxFl7S/pZuuWMyhyXup1UPR3Oirpuv3kWOsElVzGOxMWREE0eoCnGYKN2VCBx+igwQbi+x/cVSf49sFBVfdpPHUGse3KwS7ukfvpmmYm06dy2JS93JrRaCUUUw2DN8VjW7dIODv jrusz@redhat.com
 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINPExjjH74MOM6wrXEpRUg6I0dtRdAV3bAUY+u7WMc2G sanne@redhat.com
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjNynFZPCEPVDyOB2yzrww5kxwK6MAb1D0GN5yP8y/iw+gtx+Hj3CqojHMTa/9r3q3R1TMgCITdvzAiKylbx/owV8bgXS1p8je2KirWx3o/Dy80AYsas2F+sodm5/FOz6LvcUZw2vZiVs1wp8dz7ak+pm6Xg7xa7511xO4T/HStzNUE/XSPYmC9LNJ+uVWTiCjTWlZxp1JcDVfO7k69F60u8D42e1Ty60IeNeJItX/o8FUjB/rMAAJRpjFpd/uyfPTWamjNoVzrB7chFxaemg2Nf8na6PHLAx8Gcxz2fdnnsg+M5vr6z0yVYz1cc8VOhYynQm9iISvTt6bDVEbWc2T thozza@redhat.com
diff --git a/tools/koji-compose.py b/tools/koji-compose.py
index 8f95d9f6c..c61f0c27d 100755
--- a/tools/koji-compose.py
+++ b/tools/koji-compose.py
@@ -1,4 +1,7 @@
 #!/usr/bin/python3
+# pylint: disable=invalid-name
+
+import argparse
 import json
 import sys
 import time
@@ -63,7 +66,8 @@ def upload_options_by_cloud_target(cloud_target):
             "region": os.getenv("AWS_REGION"),
             "share_with_accounts": [os.getenv("AWS_API_TEST_SHARE_ACCOUNT")]
         }
-    elif cloud_target == "azure":
+
+    if cloud_target == "azure":
         return {
             # image name is currently not set for Koji composes
             # "image_name": "",
@@ -72,7 +76,8 @@ def upload_options_by_cloud_target(cloud_target):
             "subscription_id": os.getenv("AZURE_SUBSCRIPTION_ID"),
             "tenant_id": os.getenv("AZURE_TENANT_ID")
         }
-    elif cloud_target == "gcp":
+
+    if cloud_target == "gcp":
         return {
             # image name is currently not set for Koji composes
             # "image_name": "",
@@ -80,8 +85,8 @@ def upload_options_by_cloud_target(cloud_target):
             "region": os.getenv("GCP_REGION"),
             "share_with_accounts": [os.getenv("GCP_API_TEST_SHARE_ACCOUNT")]
         }
-    else:
-        raise RuntimeError(f"unsupported target cloud: {cloud_target}")
+
+    raise RuntimeError(f"unsupported target cloud: {cloud_target}")
 
 
 def compose_request_cloud_upload(distro, koji, arch, cloud_target, image_type):
@@ -113,24 +118,73 @@ def compose_request_cloud_upload(distro, koji, arch, cloud_target, image_type):
     return req
 
 
+# Client for the Composer API.
+class ComposerAPIClient:
+
+    def __init__(self, api_url, refresh_token, auth_server):
+        self.api_url = api_url
+        self.refresh_token = refresh_token
+        self.auth_server = auth_server
+
+    def access_token(self):
+        resp = requests.post(self.auth_server + "/token", data={"refresh_token": self.refresh_token})
+        if resp.status_code != 200:
+            raise RuntimeError(f"failed to refresh token: {resp.text}")
+        return resp.json()["access_token"]
+
+    def submit_compose(self, request):
+        return requests.post(self.api_url + "/compose", json=request,
+                             headers={"Authorization": f"Bearer {self.access_token()}"})
+
+    def compose_status(self, compose_id):
+        return requests.get(self.api_url + f"/composes/{compose_id}",
+                            headers={"Authorization": f"Bearer {self.access_token()}"})
+
+    def compose_log(self, compose_id):
+        return requests.get(self.api_url + f"/composes/{compose_id}/logs",
+                            headers={"Authorization": f"Bearer {self.access_token()}"})
+
+
+def get_parser():
+    parser = argparse.ArgumentParser(description="Koji compose test")
+    parser.add_argument("distro", metavar="DISTRO", help="Distribution to build")
+    parser.add_argument("arch", metavar="ARCH", help="Architecture to build")
+    parser.add_argument("--refresh-token", metavar="TOKEN", help="JWT refresh token. " +
+                        "If not provided, read from /etc/osbuild-worker/token")
+    parser.add_argument("--auth-server", default="http://localhost:8081", help="Auth server URL")
+    parser.add_argument("--koji-url", default="https://localhost:4343/kojihub", help="Koji server to use")
+    parser.add_argument("--composer-url", default="http://localhost:443/api/image-builder-composer/v2",
+                        help="Composer API server to use")
+
+    cloud_upload_group = parser.add_argument_group("Cloud upload options")
+    cloud_upload_group.add_argument("cloud_target", metavar="CLOUD-TARGET", nargs="?", help="Cloud target to use")
+    cloud_upload_group.add_argument("image_type", metavar="IMAGE-TYPE", nargs="?", help="Image type to build")
+
+    return parser
+
+
 def main():
-    koji_url = "https://localhost:4343/kojihub"
-    if len(sys.argv) == 3:
-        distro, arch = sys.argv[1], sys.argv[2]
-        print("Using simple Koji compose with 2 image requests", file=sys.stderr)
-        cr = compose_request(distro, koji_url, arch)
-    elif len(sys.argv) == 5:
-        distro, arch, cloud_target, image_type = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
-        print("Using Koji compose with 1 image requests and upload to cloud", file=sys.stderr)
-        cr = compose_request_cloud_upload(distro, koji_url, arch, cloud_target, image_type)
+    parser = get_parser()
+    args = parser.parse_args()
+
+    if args.cloud_target and not args.image_type or not args.cloud_target and args.image_type:
+        parser.error("CLOUD-TARGET and IMAGE-TYPE must be used together")
+
+    refresh_token = args.refresh_token
+    if not refresh_token:
+        with open("/etc/osbuild-worker/token", encoding="utf-8") as f:
+            refresh_token = f.read().strip()
+
+    composer_api_client = ComposerAPIClient(args.composer_url, refresh_token, args.auth_server)
+
+    if args.cloud_target is not None:
+        cr = compose_request_cloud_upload(args.distro, args.koji_url, args.arch, args.cloud_target, args.image_type)
     else:
-        print(f"usage: {sys.argv[0]} DISTRO ARCH [CLOUD_TARGET IMAGE_TYPE]", file=sys.stderr)
-        sys.exit(1)
+        cr = compose_request(args.distro, args.koji_url, args.arch)
+
     print(json.dumps(cr), file=sys.stderr)
 
-    r = requests.post("https://localhost/api/image-builder-composer/v2/compose", json=cr,
-                      cert=("/etc/osbuild-composer/worker-crt.pem", "/etc/osbuild-composer/worker-key.pem"),
-                      verify="/etc/osbuild-composer/ca-crt.pem")
+    r = composer_api_client.submit_compose(cr)
     if r.status_code != 201:
         print("Failed to create compose", file=sys.stderr)
         print(r.text, file=sys.stderr)
@@ -141,36 +195,35 @@ def main():
     print(compose_id)
 
     while True:
-        r = requests.get(f"https://localhost/api/image-builder-composer/v2/composes/{compose_id}",
-                         cert=("/etc/osbuild-composer/worker-crt.pem", "/etc/osbuild-composer/worker-key.pem"),
-                         verify="/etc/osbuild-composer/ca-crt.pem")
+        r = composer_api_client.compose_status(compose_id)
         if r.status_code != 200:
             print("Failed to get compose status", file=sys.stderr)
             print(r.text, file=sys.stderr)
             sys.exit(1)
         status = r.json()["status"]
         print(status, file=sys.stderr)
+
         if status == "success":
             print("Compose worked!", file=sys.stderr)
             print(r.text, file=sys.stderr)
             break
-        elif status == "failure":
+
+        if status == "failure":
             print("compose failed!", file=sys.stderr)
             print(r.text, file=sys.stderr)
             sys.exit(1)
-        elif status != "pending" and status != "running":
+
+        if status not in ("pending", "running"):
             print(f"unexpected status: {status}", file=sys.stderr)
             print(r.text, file=sys.stderr)
             sys.exit(1)
 
         time.sleep(10)
 
-    r = requests.get(f"https://localhost/api/image-builder-composer/v2/composes/{compose_id}/logs",
-                     cert=("/etc/osbuild-composer/worker-crt.pem", "/etc/osbuild-composer/worker-key.pem"),
-                     verify="/etc/osbuild-composer/ca-crt.pem")
+    r = composer_api_client.compose_log(compose_id)
     logs = r.json()
     assert "image_builds" in logs
-    assert type(logs["image_builds"]) == list
+    assert isinstance(logs["image_builds"], list)
     assert len(logs["image_builds"]) == len(cr["image_requests"])
 
 
diff --git a/tools/provision.sh b/tools/provision.sh
index 680d860e9..4a94f5e97 100755
--- a/tools/provision.sh
+++ b/tools/provision.sh
@@ -7,6 +7,13 @@ source /usr/libexec/osbuild-composer-test/set-env-variables.sh
 ARTIFACTS="${ARTIFACTS:=/tmp/artifacts}"
 mkdir -p "${ARTIFACTS}"
 
+# determine the authentication method used by composer
+AUTH_METHOD_TLS="tls"
+AUTH_METHOD_JWT="jwt"
+AUTH_METHOD_NONE="none"
+# default to TLS for now
+AUTH_METHOD="${1:-$AUTH_METHOD_TLS}"
+
 # koji and ansible are not in RHEL repositories. Depending on them in the spec
 # file breaks RHEL gating (see OSCI-1541). Therefore, we need to enable epel
 # and install koji and ansible here.
@@ -34,122 +41,157 @@ if [[ "$VERSION_ID" == "9.0" || "$VERSION_ID" == "9" ]]; then
 fi
 
 sudo mkdir -p /etc/osbuild-composer
-sudo cp -a /usr/share/tests/osbuild-composer/composer/osbuild-composer.toml \
-    /etc/osbuild-composer/
-
 sudo mkdir -p /etc/osbuild-worker
-sudo cp -a /usr/share/tests/osbuild-composer/worker/osbuild-worker.toml \
-    /etc/osbuild-worker/
-
-# if GCP credentials are defined in the ENV, add them to the worker's configuration
-GOOGLE_APPLICATION_CREDENTIALS="${GOOGLE_APPLICATION_CREDENTIALS:-}"
-if [ -n "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
-    # The credentials file must be copied to a different location. Jenkins places
-    # it into /tmp and as a result, the worker would not see it due to using PrivateTmp=true.
-    GCP_CREDS_WORKER_PATH="/etc/osbuild-worker/gcp-credentials.json"
-    sudo cp "$GOOGLE_APPLICATION_CREDENTIALS" "$GCP_CREDS_WORKER_PATH"
-    echo -e "\n[gcp]\ncredentials = \"$GCP_CREDS_WORKER_PATH\"\n" | sudo tee -a /etc/osbuild-worker/osbuild-worker.toml
-fi
 
-# if Azure credentials are defined in the env, create the credentials file
-V2_AZURE_CLIENT_ID="${V2_AZURE_CLIENT_ID:-}"
-V2_AZURE_CLIENT_SECRET="${V2_AZURE_CLIENT_SECRET:-}"
-if [[ -n "$V2_AZURE_CLIENT_ID" && -n "$V2_AZURE_CLIENT_SECRET" ]]; then
-    set +x
-    sudo tee /etc/osbuild-worker/azure-credentials.toml > /dev/null << EOF
+# osbuild-composer and worker need to be configured in a specific way only when using
+# some authentication method (Service scenario). In such case, also credentials for
+# interacting with cloud providers are configured directly in the worker. In addition,
+# no certificates need to be generated, because they are not used anywhere in this
+# scenario.
+if [[ "$AUTH_METHOD" != "$AUTH_METHOD_NONE" ]]; then
+    # Generate all X.509 certificates for the tests
+    # The whole generation is done in a $CADIR to better represent how osbuild-ca
+    # it.
+    CERTDIR=/etc/osbuild-composer
+    OPENSSL_CONFIG=/usr/share/tests/osbuild-composer/x509/openssl.cnf
+    CADIR=/etc/osbuild-composer-test/ca
+
+    scriptloc=$(dirname "$0")
+    sudo "${scriptloc}/gen-certs.sh" "${OPENSSL_CONFIG}" "${CERTDIR}" "${CADIR}"
+    sudo chown _osbuild-composer "${CERTDIR}"/composer-*.pem
+
+    # Copy the appropriate configuration files
+    if [[ "$AUTH_METHOD" == "$AUTH_METHOD_JWT" ]]; then
+        COMPOSER_TEST_CONFIG="/usr/share/tests/osbuild-composer/composer/osbuild-composer-jwt.toml"
+        WORKER_TEST_CONFIG="/usr/share/tests/osbuild-composer/worker/osbuild-worker-jwt.toml"
+
+        # Default orgID
+        sudo tee "/etc/osbuild-worker/token" >/dev/null <<EOF
+123456789
+EOF
+
+        /usr/libexec/osbuild-composer-test/run-mock-auth-servers.sh start
+
+    elif [[ "$AUTH_METHOD" == "$AUTH_METHOD_TLS" ]]; then
+        COMPOSER_TEST_CONFIG="/usr/share/tests/osbuild-composer/composer/osbuild-composer-tls.toml"
+        WORKER_TEST_CONFIG="/usr/share/tests/osbuild-composer/worker/osbuild-worker-tls.toml"
+    fi
+
+    sudo cp -a "$COMPOSER_TEST_CONFIG" /etc/osbuild-composer/osbuild-composer.toml
+    sudo cp -a "$WORKER_TEST_CONFIG" /etc/osbuild-worker/osbuild-worker.toml
+
+    # if GCP credentials are defined in the ENV, add them to the worker's configuration
+    GOOGLE_APPLICATION_CREDENTIALS="${GOOGLE_APPLICATION_CREDENTIALS:-}"
+    if [ -n "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
+        # The credentials file must be copied to a different location. Jenkins places
+        # it into /tmp and as a result, the worker would not see it due to using PrivateTmp=true.
+        GCP_CREDS_WORKER_PATH="/etc/osbuild-worker/gcp-credentials.json"
+        sudo cp "$GOOGLE_APPLICATION_CREDENTIALS" "$GCP_CREDS_WORKER_PATH"
+        echo -e "\n[gcp]\ncredentials = \"$GCP_CREDS_WORKER_PATH\"\n" | sudo tee -a /etc/osbuild-worker/osbuild-worker.toml
+    fi
+
+    # if Azure credentials are defined in the env, create the credentials file
+    V2_AZURE_CLIENT_ID="${V2_AZURE_CLIENT_ID:-}"
+    V2_AZURE_CLIENT_SECRET="${V2_AZURE_CLIENT_SECRET:-}"
+    if [[ -n "$V2_AZURE_CLIENT_ID" && -n "$V2_AZURE_CLIENT_SECRET" ]]; then
+        set +x
+        sudo tee /etc/osbuild-worker/azure-credentials.toml > /dev/null << EOF
 client_id =     "$V2_AZURE_CLIENT_ID"
 client_secret = "$V2_AZURE_CLIENT_SECRET"
 EOF
-    sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
+        sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
 
 [azure]
 credentials = "/etc/osbuild-worker/azure-credentials.toml"
 EOF
-    set -x
-fi
+        set -x
+    fi
 
-# if AWS credentials are defined in the ENV, add them to the worker's configuration
-V2_AWS_ACCESS_KEY_ID="${V2_AWS_ACCESS_KEY_ID:-}"
-V2_AWS_SECRET_ACCESS_KEY="${V2_AWS_SECRET_ACCESS_KEY:-}"
-if [[ -n "$V2_AWS_ACCESS_KEY_ID" && -n "$V2_AWS_SECRET_ACCESS_KEY" ]]; then
-    set +x
+    # if AWS credentials are defined in the ENV, add them to the worker's configuration
+    V2_AWS_ACCESS_KEY_ID="${V2_AWS_ACCESS_KEY_ID:-}"
+    V2_AWS_SECRET_ACCESS_KEY="${V2_AWS_SECRET_ACCESS_KEY:-}"
+    if [[ -n "$V2_AWS_ACCESS_KEY_ID" && -n "$V2_AWS_SECRET_ACCESS_KEY" ]]; then
+        set +x
     sudo tee /etc/osbuild-worker/aws-credentials.toml > /dev/null << EOF
 [default]
 aws_access_key_id = "$V2_AWS_ACCESS_KEY_ID"
 aws_secret_access_key = "$V2_AWS_SECRET_ACCESS_KEY"
 EOF
-    sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
+        sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
 
 [aws]
 credentials = "/etc/osbuild-worker/aws-credentials.toml"
 bucket = "${AWS_BUCKET}"
 EOF
-    set -x
-fi
+        set -x
+    fi
 
-# Copy rpmrepo snapshots for use in weldr tests
-REPODIR=/etc/osbuild-composer/repositories
-sudo mkdir -p $REPODIR
-# Copy all fedora repo overrides
-sudo cp -a /usr/share/tests/osbuild-composer/repositories/{fedora,centos}-*.json "$REPODIR"
-# Copy RHEL point release repos
-sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-85.json "$REPODIR"
-sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-86.json "$REPODIR"
-sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-87.json "$REPODIR"
-sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-90.json "$REPODIR"
-sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-91.json "$REPODIR"
-
-# RHEL nightly repos need to be overridden
-case "${ID}-${VERSION_ID}" in
-    "rhel-8.6")
-        # Override old rhel-8.json and rhel-8-beta.json because RHEL 8.6 test needs nightly repos
-        sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-86.json "$REPODIR/rhel-8.json"
-        # If multiple tests are run and call provision.sh the symlink will need to be overridden with -f
-        sudo ln -sf /etc/osbuild-composer/repositories/rhel-8.json "$REPODIR/rhel-8-beta.json"
-        ;;
-    "rhel-9.0")
-        # Override old rhel-90.json and rhel-90-beta.json because RHEL 9.0 test needs nightly repos
-        sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-90.json "$REPODIR/rhel-90.json"
-        # If multiple tests are run and call provision.sh the symlink will need to be overridden with -f
-        sudo ln -sf /etc/osbuild-composer/repositories/rhel-90.json "$REPODIR/rhel-90-beta.json"
-        ;;
-    *) ;;
-esac
+else # AUTH_METHOD_NONE
+    # Repositories in /etc/osbuild-composer/repositories are used only in the
+    # on-premise scenario (Weldr).
+    # Copy rpmrepo snapshots for use in weldr tests
+    REPODIR=/etc/osbuild-composer/repositories
+    sudo mkdir -p $REPODIR
+    # Copy all fedora repo overrides
+    sudo cp -a /usr/share/tests/osbuild-composer/repositories/{fedora,centos}-*.json "$REPODIR"
+    # Copy RHEL point release repos
+    sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-85.json "$REPODIR"
+    sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-86.json "$REPODIR"
+    sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-87.json "$REPODIR"
+    sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-90.json "$REPODIR"
+    sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-91.json "$REPODIR"
+
+    # RHEL nightly repos need to be overridden
+    case "${ID}-${VERSION_ID}" in
+        "rhel-8.6")
+            # Override old rhel-8.json and rhel-8-beta.json because RHEL 8.6 test needs nightly repos
+            sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-86.json "$REPODIR/rhel-8.json"
+            # If multiple tests are run and call provision.sh the symlink will need to be overridden with -f
+            sudo ln -sf /etc/osbuild-composer/repositories/rhel-8.json "$REPODIR/rhel-8-beta.json"
+            ;;
+        "rhel-9.0")
+            # Override old rhel-90.json and rhel-90-beta.json because RHEL 9.0 test needs nightly repos
+            sudo cp /usr/share/tests/osbuild-composer/repositories/rhel-90.json "$REPODIR/rhel-90.json"
+            # If multiple tests are run and call provision.sh the symlink will need to be overridden with -f
+            sudo ln -sf /etc/osbuild-composer/repositories/rhel-90.json "$REPODIR/rhel-90-beta.json"
+            ;;
+        *) ;;
+    esac
+
+    # overrides for RHEL nightly builds testing
+    VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
+    if [ -f "rhel-${VERSION_ID%.*}.json" ]; then
+        sudo cp rhel-"${VERSION_ID%.*}".json "$REPODIR/rhel-${VERSION_SUFFIX}.json"
+    fi
 
-# overrides for RHEL nightly builds testing
-VERSION_SUFFIX=$(echo "${VERSION_ID}" | tr -d ".")
-if [ -f "rhel-${VERSION_ID%.*}.json" ]; then
-    sudo cp rhel-"${VERSION_ID%.*}".json "$REPODIR/rhel-${VERSION_SUFFIX}.json"
+    if [ -f "rhel-${VERSION_ID%.*}-beta.json" ]; then
+        sudo cp rhel-"${VERSION_ID%.*}"-beta.json "$REPODIR/rhel-${VERSION_SUFFIX}-beta.json"
+    fi
 fi
 
-if [ -f "rhel-${VERSION_ID%.*}-beta.json" ]; then
-    sudo cp rhel-"${VERSION_ID%.*}"-beta.json "$REPODIR/rhel-${VERSION_SUFFIX}-beta.json"
-fi
+# start appropriate units
+case "${AUTH_METHOD}" in
+    "${AUTH_METHOD_JWT}" | "${AUTH_METHOD_TLS}")
+        # JWT / TLS are used only in the "Service" scenario. This means that:
+        # - only remote workers will be used (no local worker)
+        # - only Cloud API socket will be started (no Weldr API)
+        sudo systemctl stop 'osbuild*'
+        # make sure that the local worker is not running
+        sudo systemctl mask osbuild-worker@1.service
+        # enable remote worker API
+        sudo systemctl start osbuild-remote-worker.socket
+        # enable Cloud API
+        sudo systemctl start osbuild-composer-api.socket
+        # start a remote worker
+        sudo systemctl start osbuild-remote-worker@localhost:8700.service
+        ;;
 
-# Generate all X.509 certificates for the tests
-# The whole generation is done in a $CADIR to better represent how osbuild-ca
-# it.
-CERTDIR=/etc/osbuild-composer
-OPENSSL_CONFIG=/usr/share/tests/osbuild-composer/x509/openssl.cnf
-CADIR=/etc/osbuild-composer-test/ca
-
-scriptloc=$(dirname "$0")
-sudo "${scriptloc}/gen-certs.sh" "${OPENSSL_CONFIG}" "${CERTDIR}" "${CADIR}"
-sudo chown _osbuild-composer "${CERTDIR}"/composer-*.pem
-
-sudo systemctl start osbuild-remote-worker.socket
-sudo systemctl start osbuild-composer.socket
-sudo systemctl start osbuild-composer-api.socket
-
-# The keys were regenerated but osbuild-composer might be already running.
-# Let's try to restart it. In ideal world, this shouldn't be needed as every
-# test case is supposed to run on a pristine machine. However, this is
-# currently not true on Schutzbot
-sudo systemctl try-restart osbuild-composer
-
-# Basic verification
-sudo composer-cli status show
-sudo composer-cli sources list
-for SOURCE in $(sudo composer-cli sources list); do
-    sudo composer-cli sources info "$SOURCE"
-done
+    "${AUTH_METHOD_NONE}")
+        # No authentication method is used on-premise with Weldr. This means that:
+        # - only local worker will be used (started automatically)
+        # - only Weldr API socket will be started
+        sudo systemctl stop 'osbuild*'
+        # enable Weldr API
+        sudo systemctl start osbuild-composer.socket
+        ;;
+esac
diff --git a/tools/run-mock-auth-servers.sh b/tools/run-mock-auth-servers.sh
new file mode 100644
index 000000000..fee2dafb0
--- /dev/null
+++ b/tools/run-mock-auth-servers.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+set -eu
+
+# Colorful output.
+function greenprint {
+    echo -e "\033[1;32m[$(date -Isecond)] ${1}\033[0m"
+}
+
+servers_start() {
+    greenprint "Starting mock JWT AUTH servers"
+    # Spin up an https instance for the composer-api and worker-api; the auth handler needs to hit an ssl `/certs` endpoint
+    sudo /usr/libexec/osbuild-composer-test/osbuild-mock-openid-provider -a localhost:8082 -rsaPubPem /etc/osbuild-composer/client-crt.pem -rsaPem /etc/osbuild-composer/client-key.pem -cert /etc/osbuild-composer/composer-crt.pem -key /etc/osbuild-composer/composer-key.pem &
+    # Spin up an http instance for the worker client to bypass the need to specify an extra CA
+    sudo /usr/libexec/osbuild-composer-test/osbuild-mock-openid-provider -a localhost:8081 -rsaPubPem /etc/osbuild-composer/client-crt.pem -rsaPem /etc/osbuild-composer/client-key.pem &
+}
+
+servers_stop() {
+    greenprint "Stopping mock JWT AUTH servers"
+    local KILL_PIDS=()
+    # shellcheck disable=SC2207
+    # The split is desired and should be simple enough for the shell to handle
+    KILL_PIDS=($(pgrep -f '^sudo /usr/libexec/osbuild-composer-test/osbuild-mock-openid-provider'))
+    for PID in "${KILL_PIDS[@]}"; do
+        sudo pkill -P "$PID"
+    done
+}
+
+case "$1" in
+    "start")
+        servers_start
+        ;;
+    "stop")
+        servers_stop
+        ;;
+    *)
+        echo "Usage: $0 {start|stop}"
+        exit 1
+        ;;
+esac


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a0cbc1256..8a856b25c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -88,7 +88,7 @@ jobs:
     steps:
 
       - name: Install build and test dependencies
-        run: dnf -y install python3-pylint git-core
+        run: dnf -y install python3-pylint git-core python3-requests
 
       - name: Check out code into the Go module directory
         uses: actions/checkout@v3
@@ -97,7 +97,7 @@ jobs:
 
       - name: Analysing the code with pylint
         run: |
-          python3 -m pylint dnf-json
+          python3 -m pylint dnf-json tools/koji-compose.py
 
   lint:
     name: "⌨ Lint"
diff --git a/internal/blueprint/customizations_test.go b/internal/blueprint/customizations_test.go
index 8ab7b91f1..3dc3dc228 100644
--- a/internal/blueprint/customizations_test.go
+++ b/internal/blueprint/customizations_test.go
@@ -371,3 +371,19 @@ func TestGetFilesystemsMinSizeNonSectorSize(t *testing.T) {
 
 	assert.EqualValues(t, uint64(5632), retFilesystemsSize)
 }
+
+func TestGetOpenSCAPConfig(t *testing.T) {
+
+	expectedOscap := OpenSCAPCustomization{
+		DataStream: "test-data-stream.xml",
+		ProfileID:  "test_profile",
+	}
+
+	TestCustomizations := Customizations{
+		OpenSCAP: &expectedOscap,
+	}
+
+	retOpenSCAPCustomiztions := TestCustomizations.GetOpenSCAP()
+
+	assert.EqualValues(t, expectedOscap, *retOpenSCAPCustomiztions)
+}
diff --git a/internal/osbuild/oscap_remediation_stage_test.go b/internal/osbuild/oscap_remediation_stage_test.go
new file mode 100644
index 000000000..f8657b22e
--- /dev/null
+++ b/internal/osbuild/oscap_remediation_stage_test.go
@@ -0,0 +1,85 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewOscapRemediationStage(t *testing.T) {
+	stageOptions := &OscapRemediationStageOptions{DataDir: "/var/tmp", Config: OscapConfig{
+		Datastream: "test_stream",
+		ProfileID:  "test_profile",
+	}}
+	expectedStage := &Stage{
+		Type:    "org.osbuild.oscap.remediation",
+		Options: stageOptions,
+	}
+	actualStage := NewOscapRemediationStage(stageOptions)
+	assert.Equal(t, expectedStage, actualStage)
+}
+
+func TestOscapRemediationStageOptionsValidate(t *testing.T) {
+	tests := []struct {
+		name    string
+		options OscapRemediationStageOptions
+		err     bool
+	}{
+		{
+			name:    "empty-options",
+			options: OscapRemediationStageOptions{},
+			err:     true,
+		},
+		{
+			name: "empty-datastream",
+			options: OscapRemediationStageOptions{
+				Config: OscapConfig{
+					ProfileID: "test-profile",
+				},
+			},
+			err: true,
+		},
+		{
+			name: "empty-profile-id",
+			options: OscapRemediationStageOptions{
+				Config: OscapConfig{
+					Datastream: "test-datastream",
+				},
+			},
+			err: true,
+		},
+		{
+			name: "invalid-verbosity-level",
+			options: OscapRemediationStageOptions{
+				Config: OscapConfig{
+					Datastream:   "test-datastream",
+					ProfileID:    "test-profile",
+					VerboseLevel: "FAKE",
+				},
+			},
+			err: true,
+		},
+		{
+			name: "valid-data",
+			options: OscapRemediationStageOptions{
+				Config: OscapConfig{
+					Datastream:   "test-datastream",
+					ProfileID:    "test-profile",
+					VerboseLevel: "INFO",
+				},
+			},
+			err: false,
+		},
+	}
+	for idx, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.err {
+				assert.Errorf(t, tt.options.Config.validate(), "%q didn't return an error [idx: %d]", tt.name, idx)
+				assert.Panics(t, func() { NewOscapRemediationStage(&tt.options) })
+			} else {
+				assert.NoErrorf(t, tt.options.Config.validate(), "%q returned an error [idx: %d]", tt.name, idx)
+				assert.NotPanics(t, func() { NewOscapRemediationStage(&tt.options) })
+			}
+		})
+	}
+}
diff --git a/internal/worker/server_test.go b/internal/worker/server_test.go
index 76fa1682a..b08416e37 100644
--- a/internal/worker/server_test.go
+++ b/internal/worker/server_test.go
@@ -372,7 +372,7 @@ func TestRequestJobById(t *testing.T) {
 	depsolveJobId, err := server.EnqueueDepsolve(&worker.DepsolveJob{}, "")
 	require.NoError(t, err)
 
-	jobId, err := server.EnqueueManifestJobByID(&worker.ManifestJobByID{}, depsolveJobId, "")
+	jobId, err := server.EnqueueManifestJobByID(&worker.ManifestJobByID{}, []uuid.UUID{depsolveJobId}, "")
 	require.NoError(t, err)
 
 	test.TestRoute(t, server.Handler(), false, "POST", "/api/worker/v1/jobs", fmt.Sprintf(`{"arch":"arch","types":["%s"]}`, worker.JobTypeManifestIDOnly), http.StatusBadRequest,
@@ -742,10 +742,10 @@ func enqueueAndFinishTestJobDependencies(s *worker.Server, deps []testJob) ([]uu
 
 		case *worker.ManifestJobByID:
 			job := dep.main.(*worker.ManifestJobByID)
-			if len(depUUIDs) != 1 {
-				return nil, fmt.Errorf("exactly one dependency is expected for ManifestJobByID, got: %d", len(depUUIDs))
+			if len(depUUIDs) < 1 {
+				return nil, fmt.Errorf("at least one dependency is expected for ManifestJobByID, got: %d", len(depUUIDs))
 			}
-			id, err = s.EnqueueManifestJobByID(job, depUUIDs[0], "")
+			id, err = s.EnqueueManifestJobByID(job, depUUIDs, "")
 			if err != nil {
 				return nil, err
 			}
@@ -780,6 +780,16 @@ func enqueueAndFinishTestJobDependencies(s *worker.Server, deps []testJob) ([]uu
 				return nil, err
 			}
 
+		case *worker.ContainerResolveJob:
+			job := dep.main.(*worker.ContainerResolveJob)
+			if len(depUUIDs) != 0 {
+				return nil, fmt.Errorf("dependencies are not supported for ContainerResolveJob, got: %d", len(depUUIDs))
+			}
+			id, err = s.EnqueueContainerResolveJob(job, "")
+			if err != nil {
+				return nil, err
+			}
+
 		default:
 			return nil, fmt.Errorf("unexpected job type")
 		}
@@ -942,6 +952,71 @@ func TestJobDependencyChainErrors(t *testing.T) {
 				Reason: "empty manifest received",
 			},
 		},
+		// osbuild + manifest + depsolve + container resolve
+		// failed container resolve
+		{
+			job: testJob{
+				main: &worker.OSBuildJob{},
+				deps: []testJob{
+					{
+						main:   &worker.KojiInitJob{},
+						result: &worker.KojiInitJobResult{},
+					},
+					{
+						main: &worker.ManifestJobByID{},
+						deps: []testJob{
+							{
+								main: &worker.ContainerResolveJob{},
+								result: &worker.ContainerResolveJobResult{
+									JobResult: worker.JobResult{
+										JobError: &clienterrors.Error{
+											ID:     clienterrors.ErrorContainerResolution,
+											Reason: "remote container not found",
+										},
+									},
+								},
+							},
+							{
+								main:   &worker.DepsolveJob{},
+								result: &worker.DepsolveJobResult{},
+							},
+						},
+						result: &worker.ManifestJobByIDResult{
+							JobResult: worker.JobResult{
+								JobError: &clienterrors.Error{
+									ID:     clienterrors.ErrorContainerDependency,
+									Reason: "container dependency job failed",
+								},
+							},
+						},
+					},
+				},
+				result: &worker.OSBuildJobResult{
+					JobResult: worker.JobResult{
+						JobError: &clienterrors.Error{
+							ID:     clienterrors.ErrorManifestDependency,
+							Reason: "manifest dependency job failed",
+						},
+					},
+				},
+			},
+			expectedError: &clienterrors.Error{
+				ID:     clienterrors.ErrorManifestDependency,
+				Reason: "manifest dependency job failed",
+				Details: []*clienterrors.Error{
+					{
+						ID:     clienterrors.ErrorContainerDependency,
+						Reason: "container dependency job failed",
+						Details: []*clienterrors.Error{
+							{
+								ID:     clienterrors.ErrorContainerResolution,
+								Reason: "remote container not found",
+							},
+						},
+					},
+				},
+			},
+		},
 		// koji-init + osbuild + manifest + depsolve
 		// failed depsolve
 		{
@@ -1325,7 +1400,7 @@ func TestJobDependencyChainErrors(t *testing.T) {
 				Reason: "koji-finalize failed",
 			},
 		},
-		// koji-init + (osbuild + manifest + depsolve) + (osbuild + manifest + depsolve) + koji-finalize
+		// koji-init + (osbuild + manifest + depsolve + container resolve) + (osbuild + manifest + depsolve) + koji-finalize
 		// all passed
 		{
 			job: testJob{
@@ -1346,6 +1421,10 @@ func TestJobDependencyChainErrors(t *testing.T) {
 							{
 								main: &worker.ManifestJobByID{},
 								deps: []testJob{
+									{
+										main:   &worker.ContainerResolveJob{},
+										result: &worker.ContainerResolveJobResult{},
+									},
 									{
 										main:   &worker.DepsolveJob{},
 										result: &worker.DepsolveJobResult{},
diff --git a/test/cases/api.sh b/test/cases/api.sh
index be96376b2..58414239f 100755
--- a/test/cases/api.sh
+++ b/test/cases/api.sh
@@ -431,9 +431,9 @@ waitForState "failure"
 # crashed/stopped/killed worker should result in a failed state
 sendCompose "$REQUEST_FILE"
 waitForState "building"
-sudo systemctl stop "osbuild-worker@*"
+sudo systemctl stop "osbuild-remote-worker@*"
 waitForState "failure"
-sudo systemctl start "osbuild-worker@1"
+sudo systemctl start "osbuild-remote-worker@localhost:8700.service"
 
 # full integration case
 INIT_COMPOSES="$(collectMetrics)"
@@ -565,7 +565,7 @@ TOKEN="$(curl --request POST \
         http://localhost:443/api/image-builder-composer/v2/composes/"$COMPOSE_ID")" = "401" ]
 
 
-sudo systemctl start osbuild-remote-worker@localhost:8700.service
+sudo systemctl restart osbuild-remote-worker@localhost:8700.service
 sudo systemctl is-active --quiet osbuild-remote-worker@localhost:8700.service
 
 exit 0
diff --git a/test/cases/api/common/common.sh b/test/cases/api/common/common.sh
index 56fe11296..ba5f1c44a 100644
--- a/test/cases/api/common/common.sh
+++ b/test/cases/api/common/common.sh
@@ -44,3 +44,48 @@ function _instanceCheck() {
     echo "Not RHEL OS. Skip subscription check."
   fi
 }
+
+WORKER_REFRESH_TOKEN_PATH="/etc/osbuild-worker/token"
+
+# Fetch a JWT token.
+# The token is fetched using the refresh token configured in the worker.
+function access_token {
+  local refresh_token
+  refresh_token="$(cat $WORKER_REFRESH_TOKEN_PATH)"
+  access_token_with_org_id "$refresh_token"
+}
+
+# Fetch a JWT token.
+# The token is fetched using the refresh token provided as an argument.
+function access_token_with_org_id {
+  local refresh_token="$1"
+  curl --request POST \
+    --data "refresh_token=$refresh_token" \
+    --header "Content-Type: application/x-www-form-urlencoded" \
+    --silent \
+    --show-error \
+    --fail \
+    localhost:8081/token | jq -r .access_token
+}
+
+# Get the compose status using a JWT token.
+# The token is fetched using the refresh token configured in the worker.
+function compose_status {
+  local compose="$1"
+  local refresh_token
+  refresh_token="$(cat $WORKER_REFRESH_TOKEN_PATH)"
+  compose_status_with_org_id "$compose" "$refresh_token"
+}
+
+# Get the compose status using a JWT token.
+# The token is fetched using the refresh token provided as the second argument.
+function compose_status_with_org_id {
+  local compose="$1"
+  local refresh_token="$2"
+  curl \
+    --silent \
+    --show-error \
+    --fail \
+    --header "Authorization: Bearer $(access_token_with_org_id "$refresh_token")" \
+    "http://localhost:443/api/image-builder-composer/v2/composes/$compose"
+}
diff --git a/test/cases/api/container.registry.sh b/test/cases/api/container.registry.sh
index 423ff2489..762fa40f6 100644
--- a/test/cases/api/container.registry.sh
+++ b/test/cases/api/container.registry.sh
@@ -11,6 +11,7 @@ function checkEnv() {
 
 # Global var for ostree ref
 export OSTREE_REF="test/osbuild/edge"
+export CONTAINER_SOURCE="registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/fedora-minimal"
 
 function cleanup() {
     CONTAINER_NAME="${OSTREE_CONTAINER_NAME:-}"
@@ -34,7 +35,7 @@ domain="${CI_REGISTRY}"
 path_prefix="${CI_PROJECT_PATH}"
 EOF
 
-  sudo systemctl restart "osbuild-worker@1"
+  sudo systemctl restart "osbuild-remote-worker@localhost:8700"
 }
 
 function createReqFile() {
@@ -47,8 +48,14 @@ function createReqFile() {
         "baseurl": "$PAYLOAD_REPO_URL"
       }
     ],
+    "containers":[
+      {
+        "source": "$CONTAINER_SOURCE"
+      }
+    ],
     "packages": [
       "postgresql",
+      "podman",
       "dummy"
     ],
     "users":[
@@ -89,6 +96,7 @@ function verify() {
   OSTREE_CONTAINER_NAME=osbuild-test
   local IMAGE_URL
   IMAGE_URL=$(echo "$UPLOAD_OPTIONS" | jq -r '.url')
+
   sudo "${CONTAINER_RUNTIME}" run -d --name osbuild-test -p 8080:8080 "${IMAGE_URL}"
 
   GET_METADATA_CURL_REQUEST="curl --silent \
@@ -102,4 +110,21 @@ function verify() {
   SERVICED_OSTREE_COMMIT=$(curl http://localhost:8080/repo/refs/heads/${OSTREE_REF})
 
   test "${BUILD_OSTREE_COMMIT}" = "${SERVICED_OSTREE_COMMIT}"
+
+  # check the commit contains the container, for this we pull the commit into
+  # a repo and poke into the container storage to see if the container we put
+  # in there matches the one we expect to be in there.
+
+  local CONFIG_DIGEST
+  CONFIG_DIGEST=$(skopeo inspect --raw "docker://$CONTAINER_SOURCE" | jq -r .config.digest)
+
+  ostree init --repo=repo
+  ostree remote --repo=repo add --no-gpg-verify container http://localhost:8080/repo
+  sudo ostree pull --repo=repo container "${OSTREE_REF}"
+
+  local EMBEDDED_ID
+  EMBEDDED_ID=$(sudo ostree cat --repo=repo "${BUILD_OSTREE_COMMIT}" /usr/share/containers/storage/overlay-images/images.json | jq -r .[0].id)
+
+  echo -e "have: sha256:${EMBEDDED_ID}\nwant: ${CONFIG_DIGEST}"
+  test "sha256:${EMBEDDED_ID}" = "${CONFIG_DIGEST}"
 }
diff --git a/test/cases/api/generic.s3.sh b/test/cases/api/generic.s3.sh
index a7f6ec936..0301e0b04 100644
--- a/test/cases/api/generic.s3.sh
+++ b/test/cases/api/generic.s3.sh
@@ -89,7 +89,7 @@ region = "${MINIO_REGION}"
 bucket = "${MINIO_BUCKET}"
 EOF
 
-  sudo systemctl restart "osbuild-worker@1"
+  sudo systemctl restart "osbuild-remote-worker@localhost:8700"
 }
 
 # Unset AWS_REGION, region == "" in the request the worker will look for the generic s3
diff --git a/test/cases/aws.sh b/test/cases/aws.sh
index 30fe12db7..513b8c8b9 100755
--- a/test/cases/aws.sh
+++ b/test/cases/aws.sh
@@ -29,7 +29,7 @@ function get_build_info() {
 CONTAINER_IMAGE_CLOUD_TOOLS="quay.io/osbuild/cloud-tools:latest"
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Check available container runtime
 if which podman 2>/dev/null >&2; then
diff --git a/test/cases/aws_s3.sh b/test/cases/aws_s3.sh
index 8e30bbe2a..c3fcdf044 100755
--- a/test/cases/aws_s3.sh
+++ b/test/cases/aws_s3.sh
@@ -19,7 +19,7 @@ set -euo pipefail
 CONTAINER_IMAGE_CLOUD_TOOLS="quay.io/osbuild/cloud-tools:latest"
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Check available container runtime
 if which podman 2>/dev/null >&2; then
diff --git a/test/cases/azure.sh b/test/cases/azure.sh
index 53643b4d9..1a8c95f79 100755
--- a/test/cases/azure.sh
+++ b/test/cases/azure.sh
@@ -33,7 +33,7 @@ function get_build_info() {
 CONTAINER_IMAGE_CLOUD_TOOLS="quay.io/osbuild/cloud-tools:latest"
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Check available container runtime
 if which podman 2>/dev/null >&2; then
diff --git a/test/cases/base_tests.sh b/test/cases/base_tests.sh
index ce7762d8a..fd986b749 100755
--- a/test/cases/base_tests.sh
+++ b/test/cases/base_tests.sh
@@ -13,10 +13,13 @@ LOGS_DIRECTORY=$(mktemp --directory --tmpdir=/tmp/logs)
 PASSED_TESTS=()
 FAILED_TESTS=()
 
-TEST_CASES=(
+TEST_CASES_ON_PREM=(
   "osbuild-weldr-tests"
   "osbuild-dnf-json-tests"
   "osbuild-composer-cli-tests"
+)
+
+TEST_CASES_SERVICE=(
   "osbuild-auth-tests"
 )
 
@@ -45,13 +48,20 @@ run_test_case () {
 
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Change to the working directory.
 cd $WORKING_DIRECTORY
 
 # Run each test case.
-for TEST_CASE in "${TEST_CASES[@]}"; do
+for TEST_CASE in "${TEST_CASES_ON_PREM[@]}"; do
+    run_test_case ${TESTS_PATH}/"$TEST_CASE"
+done
+
+/usr/libexec/osbuild-composer-test/provision.sh tls
+
+# Run each test case.
+for TEST_CASE in "${TEST_CASES_SERVICE[@]}"; do
     run_test_case ${TESTS_PATH}/"$TEST_CASE"
 done
 
diff --git a/test/cases/container-embedding.sh b/test/cases/container-embedding.sh
index 98a46fdde..120be8272 100755
--- a/test/cases/container-embedding.sh
+++ b/test/cases/container-embedding.sh
@@ -21,7 +21,7 @@ source /usr/libexec/osbuild-composer-test/set-env-variables.sh
 # Provision the software under test.
 #
 
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 TEMPDIR=$(mktemp -d)
 function cleanup() {
diff --git a/test/cases/container-upload.sh b/test/cases/container-upload.sh
index 14fd95239..9f698c2da 100755
--- a/test/cases/container-upload.sh
+++ b/test/cases/container-upload.sh
@@ -21,7 +21,7 @@ source /usr/libexec/osbuild-composer-test/set-env-variables.sh
 # Provision the software under test.
 #
 
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 ARTIFACTS="${ARTIFACTS:-/tmp/artifacts}"
 TEMPDIR=$(mktemp -d)
diff --git a/test/cases/cross-distro.sh b/test/cases/cross-distro.sh
index 20612c384..9c9cc2a89 100755
--- a/test/cases/cross-distro.sh
+++ b/test/cases/cross-distro.sh
@@ -46,7 +46,7 @@ esac
 
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 echo "====> Finished Provisioning system"
 echo "====> Starting $(basename "$0")"
 
diff --git a/test/cases/filesystem.sh b/test/cases/filesystem.sh
index b3625df69..1cb86529f 100644
--- a/test/cases/filesystem.sh
+++ b/test/cases/filesystem.sh
@@ -8,7 +8,7 @@ set -euo pipefail
 source /etc/os-release
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Colorful output.
 function greenprint {
diff --git a/test/cases/gcp.sh b/test/cases/gcp.sh
index 60735c311..c5ae6de02 100755
--- a/test/cases/gcp.sh
+++ b/test/cases/gcp.sh
@@ -40,7 +40,7 @@ function get_build_info() {
 CONTAINER_IMAGE_CLOUD_TOOLS="quay.io/osbuild/cloud-tools:latest"
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Check available container runtime
 if which podman 2>/dev/null >&2; then
diff --git a/test/cases/generic_s3.sh b/test/cases/generic_s3.sh
index 1880a2bfd..aae481055 100755
--- a/test/cases/generic_s3.sh
+++ b/test/cases/generic_s3.sh
@@ -20,7 +20,7 @@ CONTAINER_MINIO_CLIENT="quay.io/minio/mc:latest"
 CONTAINER_MINIO_SERVER="quay.io/minio/minio:latest"
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Check available container runtime
 if which podman 2>/dev/null >&2; then
diff --git a/test/cases/image_tests.sh b/test/cases/image_tests.sh
index b87c26d80..44ddc1ebf 100755
--- a/test/cases/image_tests.sh
+++ b/test/cases/image_tests.sh
@@ -89,7 +89,7 @@ run_test_case () {
 }
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Change to the working directory.
 cd $WORKING_DIRECTORY
diff --git a/test/cases/installers.sh b/test/cases/installers.sh
index 3006a86d2..6102e7d88 100755
--- a/test/cases/installers.sh
+++ b/test/cases/installers.sh
@@ -9,10 +9,11 @@ set -euo pipefail
 #
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Get OS data.
 source /usr/libexec/osbuild-composer-test/set-env-variables.sh
+source /usr/libexec/tests/osbuild-composer/shared_lib.sh
 
 # Colorful output.
 function greenprint {
@@ -64,7 +65,11 @@ echo -e 'admin\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
 EOFKS
 
     echo "Writing new ISO"
-    sudo mkksiso -c "console=ttyS0,115200" "${newksfile}" "${iso}" "${newiso}"
+    if nvrGreaterOrEqual "lorax" "34.9.18"; then
+        sudo mkksiso -c "console=ttyS0,115200" --ks "${newksfile}" "${iso}" "${newiso}"
+    else
+        sudo mkksiso -c "console=ttyS0,115200" "${newksfile}" "${iso}" "${newiso}"
+    fi
 
     echo "==== NEW KICKSTART FILE ===="
     cat "${newksfile}"
diff --git a/test/cases/koji.sh b/test/cases/koji.sh
index 1c3fa1049..a7ca615ad 100755
--- a/test/cases/koji.sh
+++ b/test/cases/koji.sh
@@ -91,24 +91,34 @@ if [[ "$TEST_TYPE" == "$TEST_TYPE_CLOUD_UPLOAD" ]]; then
         set +eu
         cleanup
         sudo rm -rf "$WORKDIR"
+        /usr/libexec/osbuild-composer-test/run-mock-auth-servers.sh stop
         set -eu
     }
-    trap cleanups EXIT
 
     # install appropriate cloud environment client tool
     installClient
+else
+    # Source common functions
+    # In the common case above, this file is sourced by 'aws.sh' / 'gcp.sh' / 'azure.sh'
+    source /usr/libexec/tests/osbuild-composer/api/common/common.sh
+
+    function cleanups() {
+        greenprint "Script execution stopped or finished - Cleaning up"
+        set +eu
+        /usr/libexec/osbuild-composer-test/run-mock-auth-servers.sh stop
+        set -eu
+    }
 fi
 
+trap cleanups EXIT
+
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh jwt
 
 greenprint "Starting containers"
 sudo /usr/libexec/osbuild-composer-test/run-koji-container.sh start
 
 greenprint "Adding kerberos config"
-sudo cp \
-    /tmp/osbuild-composer-koji-test/client.keytab \
-    /etc/osbuild-composer/client.keytab
 sudo cp \
     /tmp/osbuild-composer-koji-test/client.keytab \
     /etc/osbuild-worker/client.keytab
@@ -122,10 +132,6 @@ sudo cp \
     /etc/pki/ca-trust/source/anchors/osbuild-composer-tests-ca-crt.pem
 sudo update-ca-trust
 
-greenprint "Restarting composer to pick up new config"
-sudo systemctl restart osbuild-composer
-sudo systemctl restart osbuild-worker\@1
-
 greenprint "Testing Koji"
 koji --server=http://localhost:8080/kojihub --user=osbuild --password=osbuildpass --authtype=password hello
 
@@ -156,13 +162,7 @@ esac
 if [[ "$TEST_TYPE" == "$TEST_TYPE_CLOUD_UPLOAD" ]]; then
     greenprint "Verify that image was uploaded to the cloud provider"
 
-    COMPOSE_STATUS=$(curl \
-        --silent \
-        --show-error \
-        --cacert /etc/osbuild-composer/ca-crt.pem \
-        --key /etc/osbuild-composer/client-key.pem \
-        --cert /etc/osbuild-composer/client-crt.pem \
-        "https://localhost/api/image-builder-composer/v2/composes/${COMPOSE_ID}")
+    COMPOSE_STATUS=$(compose_status "${COMPOSE_ID}")
     UPLOAD_OPTIONS=$(echo "${COMPOSE_STATUS}" | jq -r '.image_status.upload_status.options')
 
     # Authenticate with the appropriate cloud
diff --git a/test/cases/libvirt.sh b/test/cases/libvirt.sh
index 320f4f990..a53e387dc 100644
--- a/test/cases/libvirt.sh
+++ b/test/cases/libvirt.sh
@@ -9,7 +9,7 @@ set -euo pipefail
 source /usr/libexec/osbuild-composer-test/set-env-variables.sh
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Test the images
 /usr/libexec/osbuild-composer-test/libvirt_test.sh qcow2
diff --git a/test/cases/manifest_tests.sh b/test/cases/manifest_tests.sh
index 62ad8c405..91f6e8279 100755
--- a/test/cases/manifest_tests.sh
+++ b/test/cases/manifest_tests.sh
@@ -14,7 +14,7 @@ test_divider () {
 }
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Change to the working directory.
 cd $WORKING_DIRECTORY
diff --git a/test/cases/multi-tenancy.sh b/test/cases/multi-tenancy.sh
index 54bc9bfef..7982b0558 100755
--- a/test/cases/multi-tenancy.sh
+++ b/test/cases/multi-tenancy.sh
@@ -21,7 +21,10 @@ function greenprint {
 }
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh jwt
+
+# Source common functions
+source /usr/libexec/tests/osbuild-composer/api/common/common.sh
 
 greenprint "Registering clean ups"
 KILL_PIDS=()
@@ -39,6 +42,9 @@ function cleanup() {
   for P in "${KILL_PIDS[@]}"; do
       sudo pkill -P "$P"
   done
+
+  /usr/libexec/osbuild-composer-test/run-mock-auth-servers.sh stop
+
   set -eu
 }
 trap cleanup EXIT
@@ -61,59 +67,6 @@ sudo cp \
     "${OSBUILD_COMPOSER_TEST_DATA}"/kerberos/krb5-local.conf \
     /etc/krb5.conf.d/local
 
-greenprint "Configuring composer and worker"
-sudo tee "/etc/osbuild-composer/osbuild-composer.toml" >/dev/null <<EOF
-[koji]
-enable_tls = false
-enable_mtls = false
-enable_jwt = true
-jwt_keys_urls = ["https://localhost:8082/certs"]
-jwt_ca_file = "/etc/osbuild-composer/ca-crt.pem"
-jwt_acl_file = ""
-jwt_tenant_provider_fields = ["rh-org-id"]
-[worker]
-enable_artifacts = false
-enable_tls = true
-enable_mtls = false
-enable_jwt = true
-jwt_keys_urls = ["https://localhost:8082/certs"]
-jwt_ca_file = "/etc/osbuild-composer/ca-crt.pem"
-jwt_tenant_provider_fields = ["rh-org-id"]
-EOF
-
-# No compose will get this orgID
-sudo tee "/etc/osbuild-worker/token" >/dev/null <<EOF
-123456789
-EOF
-
-sudo tee "/etc/osbuild-worker/osbuild-worker.toml" >/dev/null <<EOF
-[authentication]
-oauth_url = "http://localhost:8081/token"
-client_id = "rhsm-api"
-offline_token = "/etc/osbuild-worker/token"
-
-[koji.localhost.kerberos]
-principal = "osbuild-krb@LOCAL"
-keytab = "/etc/osbuild-worker/client.keytab"
-
-[aws]
-bucket = "${AWS_BUCKET}"
-credentials = "/etc/osbuild-worker/aws-credentials.toml"
-EOF
-
-greenprint "Starting mock servers"
-# Spin up an https instance for the composer-api and worker-api; the auth handler needs to hit an ssl `/certs` endpoint
-sudo /usr/libexec/osbuild-composer-test/osbuild-mock-openid-provider -a localhost:8082 -rsaPubPem /etc/osbuild-composer/client-crt.pem -rsaPem /etc/osbuild-composer/client-key.pem -cert /etc/osbuild-composer/composer-crt.pem -key /etc/osbuild-composer/composer-key.pem &
-KILL_PIDS+=("$!")
-# Spin up an http instance for the worker client to bypass the need to specify an extra CA
-sudo /usr/libexec/osbuild-composer-test/osbuild-mock-openid-provider -a localhost:8081 -rsaPubPem /etc/osbuild-composer/client-crt.pem -rsaPem /etc/osbuild-composer/client-key.pem &
-KILL_PIDS+=("$!")
-
-greenprint "Restarting composer, stopping a local worker and starting a remote worker"
-sudo systemctl restart osbuild-composer
-sudo systemctl stop osbuild-worker@1
-sudo systemctl start osbuild-remote-worker@localhost:8700
-
 DISTRO=rhel-86
 
 function s3_request {
@@ -153,17 +106,6 @@ function koji_request {
 EOF
 }
 
-function access_token {
-  local refresh_token="$1"
-  curl --request POST \
-    --data "refresh_token=$refresh_token" \
-    --header "Content-Type: application/x-www-form-urlencoded" \
-    --silent \
-    --show-error \
-    --fail \
-    localhost:8081/token | jq -r .access_token
-}
-
 function send_compose {
   local request_file="$1"
   local refresh_token="$2"
@@ -172,28 +114,17 @@ function send_compose {
     --show-error \
     --fail \
     --header 'Content-Type: application/json' \
-    --header "Authorization: Bearer $(access_token "$refresh_token")" \
+    --header "Authorization: Bearer $(access_token_with_org_id "$refresh_token")" \
     --request POST \
     --data @"$request_file" \
     http://localhost:443/api/image-builder-composer/v2/compose | jq -r '.id'
 }
 
-function compose_status {
-  local compose="$1"
-  local refresh_token="$2"
-  curl \
-    --silent \
-    --show-error \
-    --fail \
-    --header "Authorization: Bearer $(access_token "$refresh_token")" \
-    "http://localhost:443/api/image-builder-composer/v2/composes/$compose" | jq -r '.status'
-}
-
 function assert_status {
   local compose="$1"
   local refresh_token="$2"
   local status="$3"
-  [[ $(compose_status "$compose" "$refresh_token") == "$status" ]]
+  [[ $(compose_status_with_org_id "$compose" "$refresh_token" | jq -r '.status') == "$status" ]]
 }
 
 function wait_for_status {
@@ -203,7 +134,7 @@ function wait_for_status {
   while true
   do
     local current_status
-    current_status=$(compose_status "$compose" "$refresh_token")
+    current_status=$(compose_status_with_org_id "$compose" "$refresh_token" | jq -r '.status')
 
     case "$current_status" in
       "$desired_status")
diff --git a/test/cases/oscap.sh b/test/cases/oscap.sh
new file mode 100755
index 000000000..1a132d18b
--- /dev/null
+++ b/test/cases/oscap.sh
@@ -0,0 +1,497 @@
+#!/usr/bin/bash
+set -euo pipefail
+
+OSBUILD_COMPOSER_TEST_DATA=/usr/share/tests/osbuild-composer/
+
+# Provision the software under test.
+/usr/libexec/osbuild-composer-test/provision.sh
+
+# Get OS data.
+source /etc/os-release
+
+# Colorful output.
+function greenprint {
+    echo -e "\033[1;32m[$(date -Isecond)] ${1}\033[0m"
+}
+
+# Start libvirtd and test it.
+greenprint "🚀 Starting libvirt daemon"
+sudo systemctl start libvirtd
+sudo virsh list --all > /dev/null
+
+# Set a customized dnsmasq configuration for libvirt so we always get the
+# same address on bootup.
+sudo tee /tmp/integration.xml > /dev/null << EOF
+<network>
+  <name>integration</name>
+  <uuid>1c8fe98c-b53a-4ca4-bbdb-deb0f26b3579</uuid>
+  <forward mode='nat'>
+    <nat>
+      <port start='1024' end='65535'/>
+    </nat>
+  </forward>
+  <bridge name='integration' stp='on' delay='0'/>
+  <mac address='52:54:00:36:46:ef'/>
+  <ip address='192.168.100.1' netmask='255.255.255.0'>
+    <dhcp>
+      <range start='192.168.100.2' end='192.168.100.254'/>
+      <host mac='34:49:22:B0:83:30' name='vm-bios' ip='192.168.100.50'/>
+      <host mac='34:49:22:B0:83:31' name='vm-uefi' ip='192.168.100.51'/>
+    </dhcp>
+  </ip>
+</network>
+EOF
+if ! sudo virsh net-info integration > /dev/null 2>&1; then
+    sudo virsh net-define /tmp/integration.xml
+fi
+if [[ $(sudo virsh net-info integration | grep 'Active' | awk '{print $2}') == 'no' ]]; then
+    sudo virsh net-start integration
+fi
+
+# Allow anyone in the wheel group to talk to libvirt.
+greenprint "🚪 Allowing users in wheel group to talk to libvirt"
+sudo tee /etc/polkit-1/rules.d/50-libvirt.rules > /dev/null << EOF
+polkit.addRule(function(action, subject) {
+    if (action.id == "org.libvirt.unix.manage" &&
+        subject.isInGroup("adm")) {
+            return polkit.Result.YES;
+    }
+});
+EOF
+
+TEST_UUID=$(uuidgen)
+IMAGE_KEY="oscap-${TEST_UUID}"
+HTTP_GUEST_ADDRESS=192.168.100.50
+ARTIFACTS="ci-artifacts"
+mkdir -p "${ARTIFACTS}"
+
+# Set up temporary files.
+TEMPDIR=$(mktemp -d)
+BLUEPRINT_FILE=${TEMPDIR}/blueprint.toml
+COMPOSE_START=${TEMPDIR}/compose-start-${IMAGE_KEY}.json
+COMPOSE_INFO=${TEMPDIR}/compose-info-${IMAGE_KEY}.json
+USER_DATA_FILE=${TEMPDIR}/user-data
+
+# SSH setup.
+SSH_OPTIONS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5)
+SSH_DATA_DIR=$(/usr/libexec/osbuild-composer-test/gen-ssh.sh)
+SSH_USER="admin"
+SSH_KEY=${SSH_DATA_DIR}/id_rsa
+SSH_KEY_PUB=$(cat "${SSH_KEY}".pub)
+
+case "${ID}-${VERSION_ID}" in
+    "rhel-8.7")
+        OS_VARIANT="rhel8-unknown"
+        PROFILE="xccdf_org.ssgproject.content_profile_cis"
+        DATASTREAM="/usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml"
+        ;;
+    "rhel-9.1")
+        OS_VARIANT="rhel9-unknown"
+        PROFILE="xccdf_org.ssgproject.content_profile_cis"
+        DATASTREAM="/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml"
+        ;;
+    *)
+        echo "$0 is not enabled for ${ID}-${VERSION_ID} skipping..."
+        exit 0
+        ;;
+esac
+
+tee "${USER_DATA_FILE}" > /dev/null << EOF
+#cloud-config
+users:
+    - name: "${SSH_USER}"
+      sudo: "ALL=(ALL) NOPASSWD:ALL"
+      ssh_authorized_keys:
+          - "${SSH_KEY_PUB}"
+EOF
+
+# Prepare cloud-init data.
+CLOUD_INIT_DIR=$(mktemp -d)
+cp "${OSBUILD_COMPOSER_TEST_DATA}"/cloud-init/meta-data "${CLOUD_INIT_DIR}"/
+cp "${USER_DATA_FILE}" "${CLOUD_INIT_DIR}"/
+# cp "${OSBUILD_COMPOSER_TEST_DATA}"/cloud-init/network-config "${CLOUD_INIT_DIR}"/
+
+# Set up a cloud-init ISO.
+gen_iso () {
+    CLOUD_INIT_PATH=/var/lib/libvirt/images/seed.iso
+    sudo rm -f "${CLOUD_INIT_PATH}"
+    pushd "${CLOUD_INIT_DIR}"
+      sudo mkisofs -o "${CLOUD_INIT_PATH}" -V cidata \
+        -r -J user-data meta-data > /dev/null 2>&1  
+    popd
+}
+
+# Insall necessary packages to run
+# scans and check results
+sudo dnf install -y xmlstarlet openscap-utils scap-security-guide
+
+get_build_info() {
+    key="$1"
+    fname="$2"
+    if rpm -q --quiet weldr-client; then
+        key=".body${key}"
+    fi
+    jq -r "${key}" "${fname}"
+}
+
+# Get the compose log.
+get_compose_log () {
+    COMPOSE_ID=$1
+    LOG_FILE=${ARTIFACTS}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.log
+
+    # Download the logs.
+    sudo composer-cli compose log "${COMPOSE_ID}" | tee "${LOG_FILE}" > /dev/null
+}
+
+# Get the compose metadata.
+get_compose_metadata () {
+    COMPOSE_ID=$1
+    METADATA_FILE=${ARTIFACTS}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.json
+
+    # Download the metadata.
+    sudo composer-cli compose metadata "${COMPOSE_ID}" > /dev/null
+
+    # Find the tarball and extract it.
+    TARBALL=$(basename "$(find . -maxdepth 1 -type f -name "*-metadata.tar")")
+    sudo tar -xf "${TARBALL}" -C "${TEMPDIR}"
+    sudo rm -f "${TARBALL}"
+
+    # Move the JSON file into place.
+    sudo cat "${TEMPDIR}"/"${COMPOSE_ID}".json | jq -M '.' | tee "${METADATA_FILE}" > /dev/null
+}
+
+# Build ostree image.
+build_image() {
+    blueprint_name=$1
+    image_type=$2
+
+    # Get worker unit file so we can watch the journal.
+    WORKER_UNIT=$(sudo systemctl list-units | grep -o -E "osbuild.*worker.*\.service")
+    sudo journalctl -af -n 1 -u "${WORKER_UNIT}" &
+    WORKER_JOURNAL_PID=$!
+    # Stop watching the worker journal when exiting.
+    trap 'sudo pkill -P ${WORKER_JOURNAL_PID}' EXIT
+
+    # Start the compose.
+    greenprint "🚀 Starting compose"
+    sudo composer-cli --json compose start "${blueprint_name}" "${image_type}" | tee "${COMPOSE_START}"
+    COMPOSE_ID=$(get_build_info ".build_id" "${COMPOSE_START}")
+
+    # Wait for the compose to finish.
+    greenprint "⏱ Waiting for compose to finish: ${COMPOSE_ID}"
+    while true; do
+        sudo composer-cli --json compose info "${COMPOSE_ID}" | tee "${COMPOSE_INFO}" > /dev/null
+        COMPOSE_STATUS=$(get_build_info ".queue_status" "${COMPOSE_INFO}")
+
+        # Is the compose finished?
+        if [[ ${COMPOSE_STATUS} != RUNNING ]] && [[ ${COMPOSE_STATUS} != WAITING ]]; then
+            break
+        fi
+
+        # Wait 30 seconds and try again.
+        sleep 5
+    done
+
+    # Capture the compose logs from osbuild.
+    greenprint "💬 Getting compose log and metadata"
+    get_compose_log "${COMPOSE_ID}"
+    get_compose_metadata "${COMPOSE_ID}"
+
+    # Kill the journal monitor immediately and remove the trap
+    sudo pkill -P "${WORKER_JOURNAL_PID}"
+    trap - EXIT
+
+    # Did the compose finish with success?
+    if [[ ${COMPOSE_STATUS} != FINISHED ]]; then
+        echo "Something went wrong with the compose. 😢"
+        exit 1
+    fi
+}
+
+ssh_ready () {
+    SSH_STATUS=$(sudo ssh -q "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" "${SSH_USER}@${1}" '/bin/bash -c "echo -n READY"')
+    if [[ ${SSH_STATUS} == READY ]]; then
+        echo 1
+    else
+        echo 0
+    fi
+}
+
+wait_for_ssh_up () {
+  # shellcheck disable=SC2034  # Unused variables left for readability
+  for LOOP_COUNTER in $(seq 0 45); do
+      RESULTS=$(ssh_ready "${1}")
+      if [[ ${RESULTS} == 1 ]]; then
+          echo "SSH is ready now! 🥳"
+          break
+      fi
+      sleep 20
+  done
+  if [[ ${RESULTS} == 0 ]]; then
+      clean_up "${1}"
+      echo "SSH failed to become ready 😢"
+      exit 1
+  fi
+}
+
+scan_vm() {
+  # oscap package has been installed on vm as
+  # oscap customization has been specified
+  # (the test has to be run as root)
+  sudo ssh -q "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" "${SSH_USER}@${1}" \
+    sudo oscap xccdf eval \
+    --results results.xml \
+    --profile "${PROFILE}" \
+    "${DATASTREAM}" || true # oscap returns exit code 2 for any failed rules
+}
+
+# Clean up our mess.
+clean_up () {
+    # Clear vm
+    if [[ $(sudo virsh domstate "${VIRSH_DOMAIN}") == "running" ]]; then
+        sudo virsh destroy "${VIRSH_DOMAIN}"
+    fi
+    sudo virsh undefine "${VIRSH_DOMAIN}" --nvram
+
+    # Remove qcow2 file.
+    sudo virsh vol-delete --pool images "${LIBVIRT_IMAGE_PATH}"
+}
+
+###############################
+##
+## Baseline Image
+##
+###############################
+
+# Write a blueprint for baseline image.
+tee "${BLUEPRINT_FILE}" > /dev/null << EOF
+name = "baseline"
+description = "A base image"
+version = "0.0.1"
+modules = []
+groups = []
+
+[[packages]]
+name = "openscap-scanner"
+version = "*"
+
+[[packages]]
+name = "scap-security-guide"
+version = "*"
+
+[[customizations.user]]
+name = "${SSH_USER}"
+description = "Administrator account"
+password = "\$6\$GRmb7S0p8vsYmXzH\$o0E020S.9JQGaHkszoog4ha4AQVs3sk8q0DvLjSMxoxHBKnB2FBXGQ/OkwZQfW/76ktHd0NX5nls2LPxPuUdl."
+key = "${SSH_KEY_PUB}"
+home = "/home/${SSH_USER}/"
+groups = ["wheel"]
+EOF
+
+greenprint "📄 baseline blueprint"
+cat "${BLUEPRINT_FILE}"
+
+# Prepare the blueprint for the compose.
+greenprint "📋 Preparing baseline blueprint"
+sudo composer-cli blueprints push "${BLUEPRINT_FILE}"
+sudo composer-cli blueprints depsolve baseline
+
+# Build baseline image.
+build_image baseline qcow2
+
+# Download the image
+greenprint "📥 Downloading the image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+IMAGE_FILENAME="${COMPOSE_ID}-disk.qcow2"
+
+greenprint "🖥 Create qcow2 file for virt install"
+sudo cp "${IMAGE_FILENAME}" "/var/lib/libvirt/images/${IMAGE_KEY}.qcow2"
+LIBVIRT_IMAGE_PATH=/var/lib/libvirt/images/${IMAGE_KEY}.qcow2
+sudo qemu-img resize "${LIBVIRT_IMAGE_PATH}" 20G
+
+# Ensure SELinux is happy with our new images.
+greenprint "👿 Running restorecon on image directory"
+sudo restorecon -Rv /var/lib/libvirt/images/
+
+greenprint "💿 Creating a cloud-init ISO"
+gen_iso
+
+greenprint "📋 Install baseline image"
+VIRSH_DOMAIN="${IMAGE_KEY}-baseline"
+sudo virt-install --name="${VIRSH_DOMAIN}"\
+                  --disk "${LIBVIRT_IMAGE_PATH}",format=qcow2 \
+                  --disk path="${CLOUD_INIT_PATH}",device=cdrom \
+                  --ram 3072 \
+                  --vcpus 2 \
+                  --network network=integration,mac=34:49:22:B0:83:30 \
+                  --os-type linux \
+                  --os-variant "${OS_VARIANT}" \
+                  --import \
+                  --noautoconsole \
+                  --nographics \
+                  --wait=15 \
+                  --noreboot
+
+# Installation can get stuck, destroying VM helps
+# See https://github.com/osbuild/osbuild-composer/issues/2413
+if [[ $(sudo virsh domstate "${VIRSH_DOMAIN}") == "running" ]]; then
+    sudo virsh destroy "${VIRSH_DOMAIN}"
+fi
+
+# Start VM.
+greenprint "💻 Start baseline VM"
+sudo virsh start "${VIRSH_DOMAIN}"
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+wait_for_ssh_up "${HTTP_GUEST_ADDRESS}"
+
+greenprint "🔒 Running oscap scanner"
+scan_vm "${HTTP_GUEST_ADDRESS}"
+
+greenprint "📗 Checking oscap score"
+BASELINE_SCORE=$(sudo ssh -q "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" "${SSH_USER}@${HTTP_GUEST_ADDRESS}" \
+  sudo cat results.xml \
+  | xmlstarlet sel -N x="http://checklists.nist.gov/xccdf/1.2" -t -v "//x:score"
+)
+
+# Clean up BASELINE VM
+greenprint "🧹 Clean up VM"
+clean_up
+
+###############################
+##
+## Hardened Image
+##
+###############################
+
+# Write a blueprint for hardened image.
+tee "${BLUEPRINT_FILE}" > /dev/null << EOF
+name = "hardened"
+description = "A hardened OpenSCAP image"
+version = "0.0.1"
+modules = []
+groups = []
+
+[[ packages ]]
+name = "openscap-scanner"
+version = "*"
+
+[[ packages ]]
+name = "scap-security-guide"
+version = "*"
+
+[customizations.openscap]
+profile_id = "${PROFILE}"
+datastream = "${DATASTREAM}"
+
+[[customizations.user]]
+name = "${SSH_USER}"
+description = "Administrator account"
+password = "\$6\$GRmb7S0p8vsYmXzH\$o0E020S.9JQGaHkszoog4ha4AQVs3sk8q0DvLjSMxoxHBKnB2FBXGQ/OkwZQfW/76ktHd0NX5nls2LPxPuUdl."
+key = "${SSH_KEY_PUB}"
+home = "/home/${SSH_USER}/"
+groups = ["wheel"]
+EOF
+
+greenprint "📄 hardened blueprint"
+cat "${BLUEPRINT_FILE}"
+
+# Prepare the blueprint for the compose.
+greenprint "📋 Preparing hardened blueprint"
+sudo composer-cli blueprints push "${BLUEPRINT_FILE}"
+sudo composer-cli blueprints depsolve hardened
+
+# Build hardened image.
+build_image hardened qcow2
+
+# Download the image
+greenprint "📥 Downloading the image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+IMAGE_FILENAME="${COMPOSE_ID}-disk.qcow2"
+
+greenprint "🖥 Create qcow2 file for virt install"
+sudo cp "${IMAGE_FILENAME}" "/var/lib/libvirt/images/${IMAGE_KEY}.qcow2"
+LIBVIRT_IMAGE_PATH=/var/lib/libvirt/images/${IMAGE_KEY}.qcow2
+sudo qemu-img resize "${LIBVIRT_IMAGE_PATH}" 20G
+
+# Ensure SELinux is happy with our new images.
+greenprint "👿 Running restorecon on image directory"
+sudo restorecon -Rv /var/lib/libvirt/images/
+
+greenprint "💿 Creating a cloud-init ISO"
+gen_iso
+
+greenprint "📋 Install hardened image"
+VIRSH_DOMAIN="${IMAGE_KEY}-hardened"
+sudo virt-install --name="${VIRSH_DOMAIN}"\
+                  --disk "${LIBVIRT_IMAGE_PATH}",format=qcow2 \
+                  --disk path="${CLOUD_INIT_PATH}",device=cdrom \
+                  --ram 3072 \
+                  --vcpus 2 \
+                  --network network=integration,mac=34:49:22:B0:83:30 \
+                  --os-type linux \
+                  --os-variant "${OS_VARIANT}" \
+                  --import \
+                  --nographics \
+                  --noautoconsole \
+                  --wait=15 \
+                  --noreboot
+
+# Installation can get stuck, destroying VM helps
+# See https://github.com/osbuild/osbuild-composer/issues/2413
+if [[ $(sudo virsh domstate "${VIRSH_DOMAIN}") == "running" ]]; then
+    sudo virsh destroy "${VIRSH_DOMAIN}"
+fi
+
+# Start VM.
+greenprint "💻 Start hardened VM"
+sudo virsh start "${VIRSH_DOMAIN}"
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+wait_for_ssh_up "${HTTP_GUEST_ADDRESS}"
+
+greenprint "🔒 Running oscap scanner"
+scan_vm "${HTTP_GUEST_ADDRESS}"
+
+greenprint "📗 Checking oscap score"
+HARDENED_SCORE=$(ssh -q "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" "${SSH_USER}@${HTTP_GUEST_ADDRESS}" \
+  sudo cat results.xml \
+  | xmlstarlet sel -N x="http://checklists.nist.gov/xccdf/1.2" -t -v "//x:score"
+)
+
+greenprint "📗 Checking for failed rules"
+SEVERITY=$(ssh -q "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" "${SSH_USER}@${HTTP_GUEST_ADDRESS}" \
+  sudo cat results.xml \
+  | xmlstarlet sel -N x="http://checklists.nist.gov/xccdf/1.2" -t -v "//x:rule-result[@severity='high']" \
+  | grep -c fail
+)
+
+# Clean up HARDENED VM
+greenprint "🧹 Clean up VM"
+clean_up
+
+# Remomve tmp dir.
+sudo rm -rf "${TEMPDIR}"
+
+greenprint "🎏 Checking for test result"
+echo "Baseline score: ${BASELINE_SCORE}%"
+echo "Hardened score: ${HARDENED_SCORE}%"
+
+# compare floating point numbers
+if python3 -c "exit(${HARDENED_SCORE} > ${BASELINE_SCORE})"; then
+  greenprint "❌ Failed"
+  echo "Hardened image did not improve baseline score"
+  exit 1
+fi
+
+# one grub rule fails (expected)
+# check if any other rules have failed
+if [[ ${SEVERITY} -gt 1 ]]; then
+  greenprint "❌ Failed"
+  echo "More than one oscap rule with high severity failed"
+  exit 1
+fi
+
+greenprint "💚 Success"
diff --git a/test/cases/ostree-ng.sh b/test/cases/ostree-ng.sh
index b863f6d30..e6ec12720 100755
--- a/test/cases/ostree-ng.sh
+++ b/test/cases/ostree-ng.sh
@@ -2,10 +2,11 @@
 set -euo pipefail
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Get OS data.
 source /usr/libexec/osbuild-composer-test/set-env-variables.sh
+source /usr/libexec/tests/osbuild-composer/shared_lib.sh
 
 # Colorful output.
 function greenprint {
@@ -194,7 +195,11 @@ ostree remote add --no-gpg-verify --no-sign-verify ${OSTREE_OSNAME} ${PROD_REPO_
 EOFKS
 
     echo "Writing new ISO"
-    sudo mkksiso -c "console=ttyS0,115200" "${newksfile}" "${iso}" "${newiso}"
+    if [ "${ID}" != "fedora" ] && nvrGreaterOrEqual "lorax" "34.9.18"; then
+        sudo mkksiso -c "console=ttyS0,115200" --ks "${newksfile}" "${iso}" "${newiso}"
+    else
+        sudo mkksiso -c "console=ttyS0,115200" "${newksfile}" "${iso}" "${newiso}"
+    fi
 
     echo "==== NEW KICKSTART FILE ===="
     cat "${newksfile}"
diff --git a/test/cases/ostree-raw-image.sh b/test/cases/ostree-raw-image.sh
index ed787c0a0..e1f0daad3 100755
--- a/test/cases/ostree-raw-image.sh
+++ b/test/cases/ostree-raw-image.sh
@@ -2,7 +2,7 @@
 set -euo pipefail
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Get OS data.
 source /etc/os-release
diff --git a/test/cases/ostree-rebase.sh b/test/cases/ostree-rebase.sh
index 147796f78..c6441309c 100755
--- a/test/cases/ostree-rebase.sh
+++ b/test/cases/ostree-rebase.sh
@@ -10,7 +10,7 @@ if [[ ${ID} == "rhel" ]] && ! sudo subscription-manager status; then
 fi
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Colorful output.
 function greenprint {
diff --git a/test/cases/ostree-simplified-installer.sh b/test/cases/ostree-simplified-installer.sh
index f7e790c8c..774853c19 100755
--- a/test/cases/ostree-simplified-installer.sh
+++ b/test/cases/ostree-simplified-installer.sh
@@ -2,7 +2,7 @@
 set -euo pipefail
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Get OS data.
 source /etc/os-release
diff --git a/test/cases/ostree.sh b/test/cases/ostree.sh
index 25e8d2ee6..39465e736 100755
--- a/test/cases/ostree.sh
+++ b/test/cases/ostree.sh
@@ -10,7 +10,7 @@ if [[ ${ID} == "rhel" ]] && ! sudo subscription-manager status; then
 fi
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Set os-variant and boot location used by virt-install.
 case "${ID}-${VERSION_ID}" in
diff --git a/test/cases/regression-bigiso.sh b/test/cases/regression-bigiso.sh
index 1e7a77746..a0d31396b 100644
--- a/test/cases/regression-bigiso.sh
+++ b/test/cases/regression-bigiso.sh
@@ -31,7 +31,7 @@ function get_build_info() {
 }
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 BLUEPRINT_FILE=/tmp/blueprint.toml
 COMPOSE_START=/tmp/compose-start.json
 COMPOSE_INFO=/tmp/compose-info.json
diff --git a/test/cases/regression-composer-works-behind-satellite-fallback.sh b/test/cases/regression-composer-works-behind-satellite-fallback.sh
index 53c2cf26f..51f880f58 100644
--- a/test/cases/regression-composer-works-behind-satellite-fallback.sh
+++ b/test/cases/regression-composer-works-behind-satellite-fallback.sh
@@ -47,7 +47,7 @@ if [[ "$ID" == rhel ]] && sudo subscription-manager status; then
 fi
 
 # Provision the software under tet.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Discover what system is installed on the runner
 case "${ID}" in
diff --git a/test/cases/regression-composer-works-behind-satellite.sh b/test/cases/regression-composer-works-behind-satellite.sh
index 1623f2682..7930a0ab1 100644
--- a/test/cases/regression-composer-works-behind-satellite.sh
+++ b/test/cases/regression-composer-works-behind-satellite.sh
@@ -76,7 +76,7 @@ if [[ "$ID" == rhel ]] && sudo subscription-manager status; then
 fi
 
 # Provision the software under tet.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Discover what system is installed on the runner
 case "${ID}" in
diff --git a/test/cases/regression-excluded-dependency.sh b/test/cases/regression-excluded-dependency.sh
index 94d6fae40..88ebf6185 100644
--- a/test/cases/regression-excluded-dependency.sh
+++ b/test/cases/regression-excluded-dependency.sh
@@ -36,7 +36,7 @@ function get_build_info() {
 }
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 BLUEPRINT_FILE=/tmp/blueprint.toml
 COMPOSE_START=/tmp/compose-start.json
 COMPOSE_INFO=/tmp/compose-info.json
diff --git a/test/cases/regression-include-excluded-packages.sh b/test/cases/regression-include-excluded-packages.sh
index 83829d28d..46e5a0fa8 100644
--- a/test/cases/regression-include-excluded-packages.sh
+++ b/test/cases/regression-include-excluded-packages.sh
@@ -22,7 +22,7 @@ if [[ "${ID}" == "fedora" ]]; then
 fi
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 BLUEPRINT_FILE=/tmp/blueprint.toml
 COMPOSE_START=/tmp/compose-start.json
 COMPOSE_INFO=/tmp/compose-info.json
diff --git a/test/cases/regression-insecure-repo.sh b/test/cases/regression-insecure-repo.sh
index 4c040be7f..0eef2608e 100644
--- a/test/cases/regression-insecure-repo.sh
+++ b/test/cases/regression-insecure-repo.sh
@@ -13,7 +13,7 @@ function greenprint {
 source /usr/libexec/osbuild-composer-test/set-env-variables.sh
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 greenprint "Registering clean ups"
 kill_pids=()
diff --git a/test/cases/regression-no-explicit-rootfs-definition.sh b/test/cases/regression-no-explicit-rootfs-definition.sh
index 84714cb7e..5b7fdbaaf 100644
--- a/test/cases/regression-no-explicit-rootfs-definition.sh
+++ b/test/cases/regression-no-explicit-rootfs-definition.sh
@@ -17,7 +17,7 @@ esac
 set -xeuo pipefail
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 
 function get_build_info() {
diff --git a/test/cases/vmware.sh b/test/cases/vmware.sh
index 3fda494c4..ce33d4779 100755
--- a/test/cases/vmware.sh
+++ b/test/cases/vmware.sh
@@ -32,7 +32,7 @@ if [ "$ID" != "rhel" ]; then
 fi
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 GOVC_CMD=/tmp/govc
 
diff --git a/test/data/composer/osbuild-composer-jwt.toml b/test/data/composer/osbuild-composer-jwt.toml
new file mode 100644
index 000000000..24b6364df
--- /dev/null
+++ b/test/data/composer/osbuild-composer-jwt.toml
@@ -0,0 +1,17 @@
+[koji]
+enable_tls = false
+enable_mtls = false
+enable_jwt = true
+jwt_keys_urls = ["https://localhost:8082/certs"]
+jwt_ca_file = "/etc/osbuild-composer/ca-crt.pem"
+jwt_acl_file = ""
+jwt_tenant_provider_fields = ["rh-org-id"]
+
+[worker]
+enable_artifacts = false
+enable_tls = true
+enable_mtls = false
+enable_jwt = true
+jwt_keys_urls = ["https://localhost:8082/certs"]
+jwt_ca_file = "/etc/osbuild-composer/ca-crt.pem"
+jwt_tenant_provider_fields = ["rh-org-id"]
diff --git a/test/data/composer/osbuild-composer.toml b/test/data/composer/osbuild-composer-tls.toml
similarity index 100%
rename from test/data/composer/osbuild-composer.toml
rename to test/data/composer/osbuild-composer-tls.toml
diff --git a/test/data/worker/osbuild-worker-jwt.toml b/test/data/worker/osbuild-worker-jwt.toml
new file mode 100644
index 000000000..1aee20311
--- /dev/null
+++ b/test/data/worker/osbuild-worker-jwt.toml
@@ -0,0 +1,8 @@
+[authentication]
+oauth_url = "http://localhost:8081/token"
+client_id = "rhsm-api"
+offline_token = "/etc/osbuild-worker/token"
+
+[koji.localhost.kerberos]
+principal = "osbuild-krb@LOCAL"
+keytab = "/etc/osbuild-worker/client.keytab"
diff --git a/test/data/worker/osbuild-worker.toml b/test/data/worker/osbuild-worker-tls.toml
similarity index 100%
rename from test/data/worker/osbuild-worker.toml
rename to test/data/worker/osbuild-worker-tls.toml
diff --git a/tools/generic_s3_test.sh b/tools/generic_s3_test.sh
index 0dbd0da2c..a4fa3d9f8 100755
--- a/tools/generic_s3_test.sh
+++ b/tools/generic_s3_test.sh
@@ -27,7 +27,7 @@ CONTAINER_MINIO_SERVER="quay.io/minio/minio:latest"
 CONTAINER_IMAGE_CLOUD_TOOLS="quay.io/osbuild/cloud-tools:latest"
 
 # Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh
+/usr/libexec/osbuild-composer-test/provision.sh none
 
 # Check available container runtime
 if which podman 2>/dev/null >&2; then