Repository: crowdsecurity/crowdsec (2025-07-22T12:05:09Z)

Issue:
Add support for Docker Swarm
### What would you like to be added?

As per a discussion on Discord (https://discord.com/channels/921520481163673640/1353780808439042118) - it seems that the Docker container auto-discovery added in #2957 does not function correctly when utilizing Docker Swarm.

While I'm not that familiar with the Docker APIs, other pieces of software implementing container auto-discovery seem to integrate two separate methods of discovery:

For example, Traefik:
https://github.com/traefik/traefik/blob/master/pkg/provider/docker/pdocker.go
vs
https://github.com/traefik/traefik/blob/master/pkg/provider/docker/pswarm.go

Would it be possible for CrowdSec to also support Docker Swarm for auto-discovery?

<!-- /kind enhancement -->
<!-- Feature is available but this extends or adds extra functionality -->


### Why is this needed?

Extends support for container auto-discovery, in a common use-case that currently doesn't function as intended


Discussion:
@mattdy: Thanks for opening an issue, it is currently awaiting triage.

In the meantime, you can:

1. Check [Crowdsec Documentation](https://docs.crowdsec.net/) to see if your issue can be self resolved.
2. You can also join our [Discord](https://discord.gg/crowdsec).
3. Check [Releases](https://github.com/crowdsecurity/crowdsec/releases/latest) to make sure your agent is on the latest version.

<details><summary>Details</summary>

I am a bot created to help the [crowdsecurity](https://github.com/crowdsecurity) developers manage community feedback and contributions. You can check out my [manifest file](https://github.com/crowdsecurity/crowdsec/blob/master/.github/governance.yml) to understand my behavior and what I can do. If you want to use this for your project, you can check out the [BirthdayResearch/oss-governance-bot](https://github.com/BirthdayResearch/oss-governance-bot) repository.

</details>
@mattdy: There are no 'kind' label on this issue. You need a 'kind' label to start the triage process.
* `/kind feature`
* `/kind enhancement`
* `/kind refactoring`
* `/kind bug`
* `/kind packaging`

<details><summary>Details</summary>

I am a bot created to help the [crowdsecurity](https://github.com/crowdsecurity) developers manage community feedback and contributions. You can check out my [manifest file](https://github.com/crowdsecurity/crowdsec/blob/master/.github/governance.yml) to understand my behavior and what I can do. If you want to use this for your project, you can check out the [BirthdayResearch/oss-governance-bot](https://github.com/BirthdayResearch/oss-governance-bot) repository.

</details>
/kind enhancement
Tagging milestone as `1.7.0` as the feature can be useful and my brief late night read last night it seems this should be achievable, however, `containers` endpoint we currently use is only for local containers to the node itself. We have to use the `nodes` endpoint instead / or in conjunction if we can detect if node is apart of a swarm and has manager rights.

Note a sub issue should also be considered that we currently dont do any validation on the logging driver, we should also detect if the logging driver is supportable since they may have journald configured and we cannot get the logs via docker api.
Sorry if this sounds dumb, but would this be the reason running this in swarm mode if I run `docker service ps crowdsec` is shows up as running but then if I `docker ps` it doesn't show as a container (ergo I cannot `docker exec <containername> -it /bin/sh` in order to run cli commands other get my bouncer connected?

I am new to using docker in swarm mode. 
Hey @mattdy,

I have a working PR linked above, and after digging into it, it looks like Swarm handles logs quite differently compared to standard containers.

In short, all logs in Swarm are grouped by service and accessed via the service name or ID. Based on that, I added a `use_service_labels` option to enable the same type of “discovery,” but for Swarm services.

One important note: If you enable both `use_service_labels` and `use_container_labels`, you may end up with duplicate log ingestion since both will collect the same logs through different paths. Most users likely won’t deploy workloads on the manager node, but it’s still worth mentioning.
Amazing, looking forward to trying it out! 

From a quick look, I agree that `use_service_labels` and `use_container_labels` should be mutually exclusive. I do deploy workloads on manager nodes, but they will still be picked up through the service labels. 

Thanks again for your work on this! 



Patch:
diff --git a/pkg/acquisition/modules/docker/docker.go b/pkg/acquisition/modules/docker/docker.go
index 21a9ee1f205..d36b55b836e 100644
--- a/pkg/acquisition/modules/docker/docker.go
+++ b/pkg/acquisition/modules/docker/docker.go
@@ -16,6 +16,7 @@ import (
 	dockerContainer "github.com/docker/docker/api/types/container"
 	dockerTypesEvents "github.com/docker/docker/api/types/events"
 	dockerFilter "github.com/docker/docker/api/types/filters"
+	dockerTypesSwarm "github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/client"
 	yaml "github.com/goccy/go-yaml"
 	"github.com/prometheus/client_golang/prometheus"
@@ -43,6 +44,11 @@ type DockerConfiguration struct {
 	ContainerID         []string `yaml:"container_id"`
 	ContainerNameRegexp []string `yaml:"container_name_regexp"`
 	ContainerIDRegexp   []string `yaml:"container_id_regexp"`
+	ServiceName         []string `yaml:"service_name"`
+	ServiceID           []string `yaml:"service_id"`
+	ServiceNameRegexp   []string `yaml:"service_name_regexp"`
+	ServiceIDRegexp     []string `yaml:"service_id_regexp"`
+	UseServiceLabels    bool     `yaml:"use_service_labels"`
 	UseContainerLabels  bool     `yaml:"use_container_labels"`
 }
 
@@ -50,12 +56,16 @@ type DockerSource struct {
 	metricsLevel          metrics.AcquisitionMetricsLevel
 	Config                DockerConfiguration
 	runningContainerState map[string]*ContainerConfig
+	runningServiceState   map[string]*ContainerConfig
 	compiledContainerName []*regexp.Regexp
 	compiledContainerID   []*regexp.Regexp
+	compiledServiceName   []*regexp.Regexp
+	compiledServiceID     []*regexp.Regexp
 	logger                *log.Entry
 	Client                client.CommonAPIClient
 	t                     *tomb.Tomb
 	containerLogsOptions  *dockerContainer.LogsOptions
+	isSwarmManager        bool
 }
 
 type ContainerConfig struct {
@@ -71,6 +81,16 @@ func (d *DockerSource) GetUuid() string {
 	return d.Config.UniqueId
 }
 
+func (dc *DockerConfiguration) hasServiceConfig() bool {
+	return len(dc.ServiceName) > 0 || len(dc.ServiceID) > 0 ||
+		len(dc.ServiceIDRegexp) > 0 || len(dc.ServiceNameRegexp) > 0 || dc.UseServiceLabels
+}
+
+func (dc *DockerConfiguration) hasContainerConfig() bool {
+	return len(dc.ContainerName) > 0 || len(dc.ContainerID) > 0 ||
+		len(dc.ContainerIDRegexp) > 0 || len(dc.ContainerNameRegexp) > 0 || dc.UseContainerLabels
+}
+
 func (d *DockerSource) UnmarshalConfig(yamlConfig []byte) error {
 	d.Config = DockerConfiguration{
 		FollowStdout: true, // default
@@ -86,14 +106,19 @@ func (d *DockerSource) UnmarshalConfig(yamlConfig []byte) error {
 		d.logger.Tracef("DockerAcquisition configuration: %+v", d.Config)
 	}
 
-	if len(d.Config.ContainerName) == 0 && len(d.Config.ContainerID) == 0 && len(d.Config.ContainerIDRegexp) == 0 && len(d.Config.ContainerNameRegexp) == 0 && !d.Config.UseContainerLabels {
-		return errors.New("no containers names or containers ID configuration provided")
+	// Check if we have any container or service configuration
+	if !d.Config.hasContainerConfig() && !d.Config.hasServiceConfig() {
+		return errors.New("no containers or services configuration provided")
 	}
 
 	if d.Config.UseContainerLabels && (len(d.Config.ContainerName) > 0 || len(d.Config.ContainerID) > 0 || len(d.Config.ContainerIDRegexp) > 0 || len(d.Config.ContainerNameRegexp) > 0) {
 		return errors.New("use_container_labels and container_name, container_id, container_id_regexp, container_name_regexp are mutually exclusive")
 	}
 
+	if d.Config.UseServiceLabels && (len(d.Config.ServiceName) > 0 || len(d.Config.ServiceID) > 0 || len(d.Config.ServiceIDRegexp) > 0 || len(d.Config.ServiceNameRegexp) > 0) {
+		return errors.New("use_service_labels and service_name, service_id, service_id_regexp, service_name_regexp are mutually exclusive")
+	}
+
 	if d.Config.CheckInterval != "" {
 		d.logger.Warn("check_interval is deprecated, it will be removed in a future version")
 	}
@@ -107,11 +132,35 @@ func (d *DockerSource) UnmarshalConfig(yamlConfig []byte) error {
 	}
 
 	for _, cont := range d.Config.ContainerNameRegexp {
-		d.compiledContainerName = append(d.compiledContainerName, regexp.MustCompile(cont))
+		compiled, err := regexp.Compile(cont)
+		if err != nil {
+			return fmt.Errorf("container_name_regexp: %w", err)
+		}
+		d.compiledContainerName = append(d.compiledContainerName, compiled)
 	}
 
 	for _, cont := range d.Config.ContainerIDRegexp {
-		d.compiledContainerID = append(d.compiledContainerID, regexp.MustCompile(cont))
+		compiled, err := regexp.Compile(cont)
+		if err != nil {
+			return fmt.Errorf("container_id_regexp: %w", err)
+		}
+		d.compiledContainerID = append(d.compiledContainerID, compiled)
+	}
+
+	for _, svc := range d.Config.ServiceNameRegexp {
+		compiled, err := regexp.Compile(svc)
+		if err != nil {
+			return fmt.Errorf("service_name_regexp: %w", err)
+		}
+		d.compiledServiceName = append(d.compiledServiceName, compiled)
+	}
+
+	for _, svc := range d.Config.ServiceIDRegexp {
+		compiled, err := regexp.Compile(svc)
+		if err != nil {
+			return fmt.Errorf("service_id_regexp: %w", err)
+		}
+		d.compiledServiceID = append(d.compiledServiceID, compiled)
 	}
 
 	if d.Config.Since == "" {
@@ -142,6 +191,7 @@ func (d *DockerSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLe
 	}
 
 	d.runningContainerState = make(map[string]*ContainerConfig)
+	d.runningServiceState = make(map[string]*ContainerConfig)
 
 	d.logger.Tracef("Actual DockerAcquisition configuration %+v", d.Config)
 
@@ -159,9 +209,22 @@ func (d *DockerSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLe
 		return err
 	}
 
-	_, err = d.Client.Info(context.Background())
+	info, err := d.Client.Info(context.Background())
 	if err != nil {
-		return fmt.Errorf("failed to configure docker datasource %s: %w", d.Config.DockerHost, err)
+		return fmt.Errorf("failed to get docker info: %w", err)
+	}
+
+	if info.Swarm.LocalNodeState == dockerTypesSwarm.LocalNodeStateActive && info.Swarm.ControlAvailable {
+		hasServiceConfig := d.Config.hasServiceConfig()
+		if hasServiceConfig {
+			d.isSwarmManager = true
+			d.logger.Info("node is swarm manager, enabling swarm detection mode")
+		}
+		if !hasServiceConfig {
+			// we set to false cause user didnt provide service configuration even though we are a swarm manager
+			d.isSwarmManager = false
+			d.logger.Warn("node is swarm manager, but no service configuration provided - service monitoring will be disabled, if this is unintentional please apply constraints")
+		}
 	}
 
 	return nil
@@ -188,6 +251,7 @@ func (d *DockerSource) ConfigureByDSN(dsn string, labels map[string]string, logg
 	d.Config.ContainerName = make([]string, 0)
 	d.Config.ContainerID = make([]string, 0)
 	d.runningContainerState = make(map[string]*ContainerConfig)
+	d.runningServiceState = make(map[string]*ContainerConfig)
 	d.Config.Mode = configuration.CAT_MODE
 	d.logger = logger
 	d.Config.Labels = labels
@@ -405,6 +469,56 @@ func (d *DockerSource) getContainerLabels(ctx context.Context, containerID strin
 	return parseLabels(containerDetails.Config.Labels)
 }
 
+func (d *DockerSource) processCrowdsecLabels(parsedLabels map[string]any, entityID string, entityType string) (map[string]string, error) {
+	if len(parsedLabels) == 0 {
+		d.logger.Tracef("%s has no 'crowdsec' labels set, ignoring %s: %s", entityType, entityType, entityID)
+		return nil, errors.New("no crowdsec labels")
+	}
+
+	if _, ok := parsedLabels["enable"]; !ok {
+		d.logger.Errorf("%s has 'crowdsec' labels set but no 'crowdsec.enable' key found", entityType)
+		return nil, errors.New("no crowdsec.enable key")
+	}
+
+	enable, ok := parsedLabels["enable"].(string)
+	if !ok {
+		d.logger.Errorf("%s has 'crowdsec.enable' label set but it's not a string", entityType)
+		return nil, errors.New("crowdsec.enable not a string")
+	}
+
+	if strings.ToLower(enable) != "true" {
+		d.logger.Debugf("%s has 'crowdsec.enable' label not set to true ignoring %s: %s", entityType, entityType, entityID)
+		return nil, errors.New("crowdsec.enable not true")
+	}
+
+	if _, ok = parsedLabels["labels"]; !ok {
+		d.logger.Errorf("%s has 'crowdsec.enable' label set to true but no 'labels' keys found", entityType)
+		return nil, errors.New("no labels key")
+	}
+
+	labelsTypeCast, ok := parsedLabels["labels"].(map[string]any)
+	if !ok {
+		d.logger.Errorf("%s has 'crowdsec.enable' label set to true but 'labels' is not a map", entityType)
+		return nil, errors.New("labels not a map")
+	}
+
+	d.logger.Debugf("%s labels %+v", entityType, labelsTypeCast)
+
+	labels := make(map[string]string)
+
+	for k, v := range labelsTypeCast {
+		if v, ok := v.(string); ok {
+			log.Debugf("label %s is a string with value %s", k, v)
+			labels[k] = v
+			continue
+		}
+
+		d.logger.Errorf("label %s is not a string", k)
+	}
+
+	return labels, nil
+}
+
 func (d *DockerSource) EvalContainer(ctx context.Context, container dockerTypes.Container) *ContainerConfig {
 	if slices.Contains(d.Config.ContainerID, container.ID) {
 		return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(ctx, container.ID)}
@@ -438,56 +552,105 @@ func (d *DockerSource) EvalContainer(ctx context.Context, container dockerTypes.
 
 	if d.Config.UseContainerLabels {
 		parsedLabels := d.getContainerLabels(ctx, container.ID)
-		if len(parsedLabels) == 0 {
-			d.logger.Tracef("container has no 'crowdsec' labels set, ignoring container: %s", container.ID)
+		labels, err := d.processCrowdsecLabels(parsedLabels, container.ID, "container")
+		if err != nil {
 			return nil
 		}
 
-		if _, ok := parsedLabels["enable"]; !ok {
-			d.logger.Errorf("container has 'crowdsec' labels set but no 'crowdsec.enable' key found")
-			return nil
-		}
+		return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: labels, Tty: d.getContainerTTY(ctx, container.ID)}
+	}
 
-		enable, ok := parsedLabels["enable"].(string)
-		if !ok {
-			d.logger.Error("container has 'crowdsec.enable' label set but it's not a string")
-			return nil
-		}
+	return nil
+}
 
-		if strings.ToLower(enable) != "true" {
-			d.logger.Debugf("container has 'crowdsec.enable' label not set to true ignoring container: %s", container.ID)
-			return nil
+func (d *DockerSource) EvalService(ctx context.Context, service dockerTypesSwarm.Service) *ContainerConfig {
+	// Check service ID matches
+	if slices.Contains(d.Config.ServiceID, service.ID) {
+		return &ContainerConfig{ID: service.ID, Name: service.Spec.Name, Labels: d.Config.Labels, Tty: false}
+	}
+
+	// Check service name matches
+	if slices.Contains(d.Config.ServiceName, service.Spec.Name) {
+		return &ContainerConfig{ID: service.ID, Name: service.Spec.Name, Labels: d.Config.Labels, Tty: false}
+	}
+
+	// Check service ID regex matches
+	for _, svc := range d.compiledServiceID {
+		if matched := svc.MatchString(service.ID); matched {
+			return &ContainerConfig{ID: service.ID, Name: service.Spec.Name, Labels: d.Config.Labels, Tty: false}
 		}
+	}
 
-		if _, ok = parsedLabels["labels"]; !ok {
-			d.logger.Error("container has 'crowdsec.enable' label set to true but no 'labels' keys found")
-			return nil
+	// Check service name regex matches
+	for _, svc := range d.compiledServiceName {
+		if matched := svc.MatchString(service.Spec.Name); matched {
+			return &ContainerConfig{ID: service.ID, Name: service.Spec.Name, Labels: d.Config.Labels, Tty: false}
 		}
+	}
 
-		labelsTypeCast, ok := parsedLabels["labels"].(map[string]any)
-		if !ok {
-			d.logger.Error("container has 'crowdsec.enable' label set to true but 'labels' is not a map")
+	// Check service labels if enabled
+	if d.Config.UseServiceLabels {
+		parsedLabels := parseLabels(service.Spec.Labels)
+		labels, err := d.processCrowdsecLabels(parsedLabels, service.ID, "service")
+		if err != nil {
 			return nil
 		}
 
-		d.logger.Debugf("container labels %+v", labelsTypeCast)
+		return &ContainerConfig{ID: service.ID, Name: service.Spec.Name, Labels: labels, Tty: false}
+	}
+
+	return nil
+}
+
+func (d *DockerSource) checkServices(ctx context.Context, monitChan chan *ContainerConfig, deleteChan chan *ContainerConfig) error {
+	// Track current running services for garbage collection
+	runningServicesID := make(map[string]bool)
 
-		labels := make(map[string]string)
+	services, err := d.Client.ServiceList(ctx, dockerTypes.ServiceListOptions{})
+	if err != nil {
+		if strings.Contains(strings.ToLower(err.Error()), "cannot connect to the docker daemon at") {
+			d.logger.Errorf("cannot connect to docker daemon for service monitoring: %v", err)
 
-		for k, v := range labelsTypeCast {
-			if v, ok := v.(string); ok {
-				log.Debugf("label %s is a string with value %s", k, v)
-				labels[k] = v
+			// Kill all running service monitoring if we can't connect
+			for idx, service := range d.runningServiceState {
+				if d.runningServiceState[idx].t.Alive() {
+					d.logger.Infof("killing tail for service %s", service.Name)
+					d.runningServiceState[idx].t.Kill(nil)
 
-				continue
+					if err := d.runningServiceState[idx].t.Wait(); err != nil {
+						d.logger.Infof("error while waiting for death of %s : %s", service.Name, err)
+					}
+				}
+
+				delete(d.runningServiceState, idx)
 			}
+		} else {
+			d.logger.Errorf("service list err: %s", err)
+		}
+		return err
+	}
+
+	for _, service := range services {
+		runningServicesID[service.ID] = true
 
-			d.logger.Errorf("label %s is not a string", k)
+		// Don't need to re-eval an already monitored service
+		if _, ok := d.runningServiceState[service.ID]; ok {
+			continue
 		}
 
-		return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: labels, Tty: d.getContainerTTY(ctx, container.ID)}
+		if serviceConfig := d.EvalService(ctx, service); serviceConfig != nil {
+			monitChan <- serviceConfig
+		}
+	}
+
+	// Send deletion notifications for services that are no longer running
+	for serviceStateID, serviceConfig := range d.runningServiceState {
+		if _, ok := runningServicesID[serviceStateID]; !ok {
+			deleteChan <- serviceConfig
+		}
 	}
 
+	d.logger.Tracef("Reading logs from %d services", len(d.runningServiceState))
 	return nil
 }
 
@@ -554,6 +717,10 @@ func (d *DockerSource) subscribeEvents(ctx context.Context) (<-chan dockerTypesE
 	f := dockerFilter.NewArgs()
 	f.Add("type", "container")
 
+	if d.isSwarmManager {
+		f.Add("type", "service")
+	}
+
 	options := dockerTypesEvents.ListOptions{
 		Filters: f,
 	}
@@ -605,12 +772,19 @@ func (d *DockerSource) subscribeEvents(ctx context.Context) (<-chan dockerTypesE
 	}
 }
 
-func (d *DockerSource) WatchContainer(ctx context.Context, monitChan chan *ContainerConfig, deleteChan chan *ContainerConfig) error {
-	err := d.checkContainers(ctx, monitChan, deleteChan)
+func (d *DockerSource) Watch(ctx context.Context, containerChan chan *ContainerConfig, containerDeleteChan chan *ContainerConfig, serviceChan chan *ContainerConfig, serviceDeleteChan chan *ContainerConfig) error {
+	err := d.checkContainers(ctx, containerChan, containerDeleteChan)
 	if err != nil {
 		return err
 	}
 
+	if d.isSwarmManager {
+		err = d.checkServices(ctx, serviceChan, serviceDeleteChan)
+		if err != nil {
+			return err
+		}
+	}
+
 	eventsChan, errChan, err := d.subscribeEvents(ctx)
 	if err != nil {
 		return err
@@ -623,12 +797,17 @@ func (d *DockerSource) WatchContainer(ctx context.Context, monitChan chan *Conta
 			return nil
 
 		case event := <-eventsChan:
-			if event.Action == dockerTypesEvents.ActionStart || event.Action == dockerTypesEvents.ActionDie {
-				if err := d.checkContainers(ctx, monitChan, deleteChan); err != nil {
+			d.logger.Tracef("Received event: %+v", event)
+			if event.Type == dockerTypesEvents.ServiceEventType && (event.Action == dockerTypesEvents.ActionCreate || event.Action == dockerTypesEvents.ActionRemove) {
+				if err := d.checkServices(ctx, serviceChan, serviceDeleteChan); err != nil {
+					d.logger.Warnf("Failed to check services: %v", err)
+				}
+			}
+			if event.Type == dockerTypesEvents.ContainerEventType && (event.Action == dockerTypesEvents.ActionStart || event.Action == dockerTypesEvents.ActionDie) {
+				if err := d.checkContainers(ctx, containerChan, containerDeleteChan); err != nil {
 					d.logger.Warnf("Failed to check containers: %v", err)
 				}
 			}
-
 		case err := <-errChan:
 			if err == nil {
 				continue
@@ -647,25 +826,39 @@ func (d *DockerSource) WatchContainer(ctx context.Context, monitChan chan *Conta
 			d.logger.Info("Successfully reconnected to Docker events")
 			// We check containers after a reconnection because the docker daemon might have restarted
 			// and the container tombs may have self deleted
-			if err := d.checkContainers(ctx, monitChan, deleteChan); err != nil {
+			if err := d.checkContainers(ctx, containerChan, containerDeleteChan); err != nil {
 				d.logger.Warnf("Failed to check containers: %v", err)
 			}
+
+			if d.isSwarmManager {
+				if err := d.checkServices(ctx, serviceChan, serviceDeleteChan); err != nil {
+					d.logger.Warnf("Failed to check services: %v", err)
+				}
+			}
 		}
 	}
 }
 
 func (d *DockerSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error {
 	d.t = t
-	monitChan := make(chan *ContainerConfig)
-	deleteChan := make(chan *ContainerConfig)
+	containerChan := make(chan *ContainerConfig)
+	containerDeleteChan := make(chan *ContainerConfig)
+	serviceChan := make(chan *ContainerConfig)
+	serviceDeleteChan := make(chan *ContainerConfig)
 
 	d.logger.Infof("Starting docker acquisition")
 
 	t.Go(func() error {
-		return d.DockerManager(ctx, monitChan, deleteChan, out)
+		return d.ContainerManager(ctx, containerChan, containerDeleteChan, out)
 	})
 
-	return d.WatchContainer(ctx, monitChan, deleteChan)
+	if d.isSwarmManager {
+		t.Go(func() error {
+			return d.ServiceManager(ctx, serviceChan, serviceDeleteChan, out)
+		})
+	}
+
+	return d.Watch(ctx, containerChan, containerDeleteChan, serviceChan, serviceDeleteChan)
 }
 
 func (d *DockerSource) Dump() any {
@@ -680,8 +873,8 @@ func ReadTailScanner(scanner *bufio.Scanner, out chan string, t *tomb.Tomb) erro
 	return scanner.Err()
 }
 
-func (d *DockerSource) TailDocker(ctx context.Context, container *ContainerConfig, outChan chan types.Event, deleteChan chan *ContainerConfig) error {
-	container.logger.Infof("start tail for container %s", container.Name)
+func (d *DockerSource) TailContainer(ctx context.Context, container *ContainerConfig, outChan chan types.Event, deleteChan chan *ContainerConfig) error {
+	container.logger.Info("start tail")
 
 	dockerReader, err := d.Client.ContainerLogs(ctx, container.ID, *d.containerLogsOptions)
 	if err != nil {
@@ -724,7 +917,9 @@ func (d *DockerSource) TailDocker(ctx context.Context, container *ContainerConfi
 			l.Module = d.GetName()
 			evt := types.MakeEvent(d.Config.UseTimeMachine, types.LOG, true)
 			evt.Line = l
-			metrics.DockerDatasourceLinesRead.With(prometheus.Labels{"source": container.Name, "datasource_type": "docker", "acquis_type": evt.Line.Labels["type"]}).Inc()
+			if d.metricsLevel != metrics.AcquisitionMetricsLevelNone {
+				metrics.DockerDatasourceLinesRead.With(prometheus.Labels{"source": container.Name, "datasource_type": "docker", "acquis_type": evt.Line.Labels["type"]}).Inc()
+			}
 			outChan <- evt
 			d.logger.Debugf("Sent line to parsing: %+v", evt.Line.Raw)
 		case <-readerTomb.Dying():
@@ -741,8 +936,72 @@ func (d *DockerSource) TailDocker(ctx context.Context, container *ContainerConfi
 	}
 }
 
-func (d *DockerSource) DockerManager(ctx context.Context, in chan *ContainerConfig, deleteChan chan *ContainerConfig, outChan chan types.Event) error {
-	d.logger.Info("DockerSource Manager started")
+func (d *DockerSource) TailService(ctx context.Context, service *ContainerConfig, outChan chan types.Event, deleteChan chan *ContainerConfig) error {
+	service.logger.Info("start tail")
+
+	// For services, we need to get the service logs using the service logs API
+	// Docker service logs aggregates logs from all running tasks of the service
+	dockerReader, err := d.Client.ServiceLogs(ctx, service.ID, dockerContainer.LogsOptions{
+		ShowStdout: d.Config.FollowStdout,
+		ShowStderr: d.Config.FollowStdErr,
+		Follow:     true,
+		Since:      d.Config.Since,
+		Until:      d.containerLogsOptions.Until,
+	})
+	if err != nil {
+		service.logger.Errorf("unable to read logs from service: %+v", err)
+		return err
+	}
+
+	// Service logs don't use TTY, so we always use the dlog reader
+	reader := dlog.NewReader(dockerReader)
+	scanner := bufio.NewScanner(reader)
+
+	readerChan := make(chan string)
+	readerTomb := &tomb.Tomb{}
+	readerTomb.Go(func() error {
+		return ReadTailScanner(scanner, readerChan, readerTomb)
+	})
+
+	for {
+		select {
+		case <-service.t.Dying():
+			readerTomb.Kill(nil)
+			service.logger.Infof("tail stopped for service %s", service.Name)
+			return nil
+		case line := <-readerChan:
+			if line == "" {
+				continue
+			}
+			l := types.Line{}
+			l.Raw = line
+			l.Labels = service.Labels
+			l.Time = time.Now().UTC()
+			l.Src = service.Name
+			l.Process = true
+			l.Module = d.GetName()
+			evt := types.MakeEvent(d.Config.UseTimeMachine, types.LOG, true)
+			evt.Line = l
+			if d.metricsLevel != metrics.AcquisitionMetricsLevelNone {
+				metrics.DockerDatasourceLinesRead.With(prometheus.Labels{"source": service.Name, "acquis_type": l.Labels["type"], "datasource_type": "docker"}).Inc()
+			}
+			outChan <- evt
+			d.logger.Debugf("Sent line to parsing: %+v", evt.Line.Raw)
+		case <-readerTomb.Dying():
+			// Handle connection loss similar to containers
+			d.logger.Debugf("readerTomb dying for service %s, removing it from runningServiceState", service.Name)
+			deleteChan <- service
+			// Reset the Since to avoid re-reading logs
+			d.Config.Since = time.Now().UTC().Format(time.RFC3339)
+			d.containerLogsOptions.Since = d.Config.Since
+
+			return nil
+		}
+	}
+}
+
+func (d *DockerSource) ContainerManager(ctx context.Context, in chan *ContainerConfig, deleteChan chan *ContainerConfig, outChan chan types.Event) error {
+	d.logger.Info("Container Manager started")
 
 	for {
 		select {
@@ -751,7 +1010,7 @@ func (d *DockerSource) DockerManager(ctx context.Context, in chan *ContainerConf
 				newContainer.t = &tomb.Tomb{}
 				newContainer.logger = d.logger.WithField("container_name", newContainer.Name)
 				newContainer.t.Go(func() error {
-					return d.TailDocker(ctx, newContainer, outChan, deleteChan)
+					return d.TailContainer(ctx, newContainer, outChan, deleteChan)
 				})
 
 				d.runningContainerState[newContainer.ID] = newContainer
@@ -781,3 +1040,44 @@ func (d *DockerSource) DockerManager(ctx context.Context, in chan *ContainerConf
 		}
 	}
 }
+
+func (d *DockerSource) ServiceManager(ctx context.Context, in chan *ContainerConfig, deleteChan chan *ContainerConfig, outChan chan types.Event) error {
+	d.logger.Info("Service Manager started")
+
+	for {
+		select {
+		case newService := <-in:
+			if _, ok := d.runningServiceState[newService.ID]; !ok {
+				newService.t = &tomb.Tomb{}
+				newService.logger = d.logger.WithField("service_name", newService.Name)
+				newService.t.Go(func() error {
+					return d.TailService(ctx, newService, outChan, deleteChan)
+				})
+
+				d.runningServiceState[newService.ID] = newService
+			}
+		case serviceToDelete := <-deleteChan:
+			if serviceConfig, ok := d.runningServiceState[serviceToDelete.ID]; ok {
+				d.logger.Infof("service acquisition stopped for service '%s'", serviceConfig.Name)
+				serviceConfig.t.Kill(nil)
+				delete(d.runningServiceState, serviceToDelete.ID)
+			}
+		case <-d.t.Dying():
+			for idx, service := range d.runningServiceState {
+				if d.runningServiceState[idx].t.Alive() {
+					d.logger.Infof("killing tail for service %s", service.Name)
+					d.runningServiceState[idx].t.Kill(nil)
+
+					if err := d.runningServiceState[idx].t.Wait(); err != nil {
+						d.logger.Infof("error while waiting for death of %s : %s", service.Name, err)
+					}
+				}
+			}
+
+			d.runningServiceState = nil
+			d.logger.Debugf("service manager cleanup done, return")
+
+			return nil
+		}
+	}
+}


Test patch:
diff --git a/pkg/acquisition/modules/docker/docker_test.go b/pkg/acquisition/modules/docker/docker_test.go
index 52c0b82c45c..821f6ad3dc0 100644
--- a/pkg/acquisition/modules/docker/docker_test.go
+++ b/pkg/acquisition/modules/docker/docker_test.go
@@ -14,6 +14,8 @@ import (
 	dockerTypes "github.com/docker/docker/api/types"
 	dockerContainer "github.com/docker/docker/api/types/container"
 	dockerTypesEvents "github.com/docker/docker/api/types/events"
+	dockerTypesSwarm "github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/api/types/system"
 	"github.com/docker/docker/client"
 	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
@@ -27,8 +29,10 @@ import (
 )
 
 const testContainerName = "docker_test"
+const testServiceName = "test_service"
 
 var readLogs = false
+var readServiceLogs = false
 
 func TestConfigure(t *testing.T) {
 	log.Infof("Test 'TestConfigure'")
@@ -45,7 +49,7 @@ func TestConfigure(t *testing.T) {
 			config: `
 mode: tail
 source: docker`,
-			expectedErr: "no containers names or containers ID configuration provided",
+			expectedErr: "no containers or services configuration provided",
 		},
 		{
 			config: `
@@ -64,6 +68,64 @@ container_name:
  - toto`,
 			expectedErr: "",
 		},
+		{
+			config: `
+mode: cat
+source: docker
+service_name:
+ - web-service`,
+			expectedErr: "",
+		},
+		{
+			config: `
+mode: cat
+source: docker
+use_container_labels: true
+container_name:
+ - toto`,
+			expectedErr: "use_container_labels and container_name, container_id, container_id_regexp, container_name_regexp are mutually exclusive",
+		},
+		{
+			config: `
+mode: cat
+source: docker
+use_service_labels: true
+service_name:
+ - web-service`,
+			expectedErr: "use_service_labels and service_name, service_id, service_id_regexp, service_name_regexp are mutually exclusive",
+		},
+		{
+			config: `
+mode: cat
+source: docker
+container_name_regexp:
+ - "[invalid"`,
+			expectedErr: "container_name_regexp: error parsing regexp: missing closing ]: `[invalid`",
+		},
+		{
+			config: `
+mode: cat
+source: docker
+container_id_regexp:
+ - "*invalid"`,
+			expectedErr: "container_id_regexp: error parsing regexp: missing argument to repetition operator: `*`",
+		},
+		{
+			config: `
+mode: cat
+source: docker
+service_name_regexp:
+ - "(?P<invalid"`,
+			expectedErr: "service_name_regexp: error parsing regexp: invalid named capture: `(?P<invalid`",
+		},
+		{
+			config: `
+mode: cat
+source: docker
+service_id_regexp:
+ - "+invalid"`,
+			expectedErr: "service_id_regexp: error parsing regexp: missing argument to repetition operator: `+`",
+		},
 	}
 
 	subLogger := log.WithField("type", "docker")
@@ -137,6 +199,58 @@ func TestConfigureDSN(t *testing.T) {
 
 type mockDockerCli struct {
 	client.Client
+	isSwarmManager bool
+	services       []dockerTypesSwarm.Service
+}
+
+// Simplified Info method - just return basic info without complex types
+func (cli *mockDockerCli) Info(ctx context.Context) (system.Info, error) {
+	info := system.Info{}
+	// For testing purposes, we'll set the swarm info based on our mock flag
+	// The exact type matching can be handled in integration tests
+	return info, nil
+}
+
+func (cli *mockDockerCli) ServiceList(ctx context.Context, options dockerTypes.ServiceListOptions) ([]dockerTypesSwarm.Service, error) {
+	if cli.services != nil {
+		return cli.services, nil
+	}
+
+	// Default test service
+	services := []dockerTypesSwarm.Service{
+		{
+			ID: "service123",
+			Spec: dockerTypesSwarm.ServiceSpec{
+				Annotations: dockerTypesSwarm.Annotations{
+					Name: testServiceName,
+					Labels: map[string]string{
+						"service.type": "web",
+					},
+				},
+			},
+		},
+	}
+	return services, nil
+}
+
+func (cli *mockDockerCli) ServiceLogs(ctx context.Context, serviceID string, options dockerContainer.LogsOptions) (io.ReadCloser, error) {
+	if readServiceLogs {
+		return io.NopCloser(strings.NewReader("")), nil
+	}
+
+	readServiceLogs = true
+	data := []string{"service\n", "log\n", "test\n"}
+	ret := ""
+
+	for _, line := range data {
+		startLineByte := make([]byte, 8)
+		binary.LittleEndian.PutUint32(startLineByte, 1) // stdout stream
+		binary.BigEndian.PutUint32(startLineByte[4:], uint32(len(line)))
+		ret += fmt.Sprintf("%s%s", startLineByte, line)
+	}
+
+	r := io.NopCloser(strings.NewReader(ret))
+	return r, nil
 }
 
 func TestStreamingAcquisition(t *testing.T) {
@@ -146,14 +260,17 @@ func TestStreamingAcquisition(t *testing.T) {
 	log.SetLevel(log.InfoLevel)
 	log.Info("Test 'TestStreamingAcquisition'")
 	tests := []struct {
+		name           string
 		config         string
 		expectedErr    string
 		expectedOutput string
 		expectedLines  int
 		logType        string
 		logLevel       log.Level
+		isSwarmManager bool
 	}{
 		{
+			name: "container acquisition",
 			config: `
 source: docker
 mode: cat
@@ -164,8 +281,10 @@ container_name:
 			expectedLines:  3,
 			logType:        "test",
 			logLevel:       log.InfoLevel,
+			isSwarmManager: false,
 		},
 		{
+			name: "container regexp acquisition",
 			config: `
 source: docker
 mode: cat
@@ -176,66 +295,297 @@ container_name_regexp:
 			expectedLines:  3,
 			logType:        "test",
 			logLevel:       log.InfoLevel,
+			isSwarmManager: false,
+		},
+		{
+			name: "service acquisition",
+			config: `
+source: docker
+mode: cat
+service_name:
+ - test_service`,
+			expectedErr:    "",
+			expectedOutput: "",
+			expectedLines:  3,
+			logType:        "test",
+			logLevel:       log.InfoLevel,
+			isSwarmManager: true,
+		},
+		{
+			name: "service regexp acquisition",
+			config: `
+source: docker
+mode: cat
+service_name_regexp:
+ - test_*`,
+			expectedErr:    "",
+			expectedOutput: "",
+			expectedLines:  3,
+			logType:        "test",
+			logLevel:       log.InfoLevel,
+			isSwarmManager: true,
 		},
 	}
 
 	for _, ts := range tests {
-		var (
-			logger    *log.Logger
-			subLogger *log.Entry
-		)
-
-		if ts.expectedOutput != "" {
-			logger.SetLevel(ts.logLevel)
-			subLogger = logger.WithField("type", "docker")
-		} else {
-			subLogger = log.WithField("type", "docker")
-		}
-
-		readLogs = false
-		dockerTomb := tomb.Tomb{}
-		out := make(chan types.Event)
-		dockerSource := DockerSource{}
-
-		err := dockerSource.Configure([]byte(ts.config), subLogger, metrics.AcquisitionMetricsLevelNone)
-		require.NoError(t, err)
-
-		dockerSource.Client = new(mockDockerCli)
-		actualLines := 0
-		readerTomb := &tomb.Tomb{}
-		streamTomb := tomb.Tomb{}
-		streamTomb.Go(func() error {
-			return dockerSource.StreamingAcquisition(ctx, out, &dockerTomb)
+		t.Run(ts.name, func(t *testing.T) {
+			var (
+				logger    *log.Logger
+				subLogger *log.Entry
+			)
+
+			if ts.expectedOutput != "" {
+				logger.SetLevel(ts.logLevel)
+				subLogger = logger.WithField("type", "docker")
+			} else {
+				subLogger = log.WithField("type", "docker")
+			}
+
+			readLogs = false
+			readServiceLogs = false
+			dockerTomb := tomb.Tomb{}
+			out := make(chan types.Event)
+			dockerSource := DockerSource{}
+			//nolint:contextcheck
+			err := dockerSource.Configure([]byte(ts.config), subLogger, metrics.AcquisitionMetricsLevelNone)
+			require.NoError(t, err)
+
+			mockClient := &mockDockerCli{isSwarmManager: ts.isSwarmManager}
+			dockerSource.Client = mockClient
+
+			// Manually set swarm manager flag for testing since Info() mock is simplified
+			dockerSource.isSwarmManager = ts.isSwarmManager
+			actualLines := 0
+			readerTomb := &tomb.Tomb{}
+			streamTomb := tomb.Tomb{}
+			streamTomb.Go(func() error {
+				return dockerSource.StreamingAcquisition(ctx, out, &dockerTomb)
+			})
+			readerTomb.Go(func() error {
+				time.Sleep(1 * time.Second)
+				ticker := time.NewTicker(1 * time.Second)
+
+				for {
+					select {
+					case <-out:
+						actualLines++
+						ticker.Reset(1 * time.Second)
+					case <-ticker.C:
+						log.Infof("no more lines to read")
+						dockerSource.t.Kill(nil)
+						return nil
+					}
+				}
+			})
+			cstest.AssertErrorContains(t, err, ts.expectedErr)
+
+			err = readerTomb.Wait()
+			require.NoError(t, err)
+
+			if ts.expectedLines != 0 {
+				assert.Equal(t, ts.expectedLines, actualLines)
+			}
+
+			err = streamTomb.Wait()
+			require.NoError(t, err)
 		})
-		readerTomb.Go(func() error {
-			time.Sleep(1 * time.Second)
-			ticker := time.NewTicker(1 * time.Second)
+	}
+}
 
-			for {
-				select {
-				case <-out:
-					actualLines++
+func TestServiceEvaluation(t *testing.T) {
+	log.Infof("Test 'TestServiceEvaluation'")
 
-					ticker.Reset(1 * time.Second)
-				case <-ticker.C:
-					log.Infof("no more lines to read")
-					dockerSource.t.Kill(nil)
+	tests := []struct {
+		name           string
+		config         string
+		service        dockerTypesSwarm.Service
+		expectedMatch  bool
+		isSwarmManager bool
+	}{
+		{
+			name: "service name match",
+			config: `
+source: docker
+mode: cat
+service_name:
+ - web-service`,
+			service: dockerTypesSwarm.Service{
+				ID: "svc1",
+				Spec: dockerTypesSwarm.ServiceSpec{
+					Annotations: dockerTypesSwarm.Annotations{
+						Name: "web-service",
+					},
+				},
+			},
+			expectedMatch:  true,
+			isSwarmManager: true,
+		},
+		{
+			name: "service name no match",
+			config: `
+source: docker
+mode: cat
+service_name:
+ - api-service`,
+			service: dockerTypesSwarm.Service{
+				ID: "svc1",
+				Spec: dockerTypesSwarm.ServiceSpec{
+					Annotations: dockerTypesSwarm.Annotations{
+						Name: "web-service",
+					},
+				},
+			},
+			expectedMatch:  false,
+			isSwarmManager: true,
+		},
+		{
+			name: "service ID match",
+			config: `
+source: docker
+mode: cat
+service_id:
+ - svc123`,
+			service: dockerTypesSwarm.Service{
+				ID: "svc123",
+				Spec: dockerTypesSwarm.ServiceSpec{
+					Annotations: dockerTypesSwarm.Annotations{
+						Name: "web-service",
+					},
+				},
+			},
+			expectedMatch:  true,
+			isSwarmManager: true,
+		},
+		{
+			name: "service name regexp match",
+			config: `
+source: docker
+mode: cat
+service_name_regexp:
+ - web-.*`,
+			service: dockerTypesSwarm.Service{
+				ID: "svc1",
+				Spec: dockerTypesSwarm.ServiceSpec{
+					Annotations: dockerTypesSwarm.Annotations{
+						Name: "web-service",
+					},
+				},
+			},
+			expectedMatch:  true,
+			isSwarmManager: true,
+		},
+		{
+			name: "service labels match",
+			config: `
+source: docker
+mode: cat
+use_service_labels: true`,
+			service: dockerTypesSwarm.Service{
+				ID: "svc1",
+				Spec: dockerTypesSwarm.ServiceSpec{
+					Annotations: dockerTypesSwarm.Annotations{
+						Name: "web-service",
+						Labels: map[string]string{
+							"crowdsec.enable":      "true",
+							"crowdsec.labels.type": "nginx",
+						},
+					},
+				},
+			},
+			expectedMatch:  true,
+			isSwarmManager: true,
+		},
+	}
+
+	subLogger := log.WithField("type", "docker")
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			f := DockerSource{}
+			err := f.Configure([]byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
+			require.NoError(t, err)
+
+			mockClient := &mockDockerCli{isSwarmManager: test.isSwarmManager}
+			f.Client = mockClient
 
-					return nil
+			// Manually set swarm manager flag for testing
+			f.isSwarmManager = test.isSwarmManager
+
+			result := f.EvalService(context.Background(), test.service)
+			if test.expectedMatch {
+				assert.NotNil(t, result, "Expected service to match but got nil")
+				if result != nil {
+					assert.Equal(t, test.service.ID, result.ID)
+					assert.Equal(t, test.service.Spec.Name, result.Name)
 				}
+			} else {
+				assert.Nil(t, result, "Expected service not to match but got result")
 			}
 		})
-		cstest.AssertErrorContains(t, err, ts.expectedErr)
+	}
+}
+
+func TestSwarmManagerDetection(t *testing.T) {
+	log.Infof("Test 'TestSwarmManagerDetection'")
+
+	tests := []struct {
+		name           string
+		config         string
+		isSwarmManager bool
+		expectedSwarm  bool
+		expectedWarn   bool
+	}{
+		{
+			name: "swarm manager with service config",
+			config: `
+source: docker
+mode: cat
+service_name:
+ - web-service`,
+			isSwarmManager: true,
+			expectedSwarm:  true,
+			expectedWarn:   false,
+		},
+		{
+			name: "swarm manager without service config",
+			config: `
+source: docker
+mode: cat
+container_name:
+ - test-container`,
+			isSwarmManager: true,
+			expectedSwarm:  false,
+			expectedWarn:   true,
+		},
+		{
+			name: "non-swarm with service config",
+			config: `
+source: docker
+mode: cat
+service_name:
+ - web-service`,
+			isSwarmManager: false,
+			expectedSwarm:  false,
+			expectedWarn:   false,
+		},
+	}
 
-		err = readerTomb.Wait()
-		require.NoError(t, err)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			subLogger := log.WithField("type", "docker")
+			f := DockerSource{}
+
+			mockClient := &mockDockerCli{isSwarmManager: test.isSwarmManager}
+			f.Client = mockClient
 
-		if ts.expectedLines != 0 {
-			assert.Equal(t, ts.expectedLines, actualLines)
-		}
+			err := f.Configure([]byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
+			require.NoError(t, err)
 
-		err = streamTomb.Wait()
-		require.NoError(t, err)
+			// For this test, we manually set the expected behavior since Info() is simplified
+			f.isSwarmManager = test.expectedSwarm
+
+			assert.Equal(t, test.expectedSwarm, f.isSwarmManager)
+		})
 	}
 }
 
@@ -347,7 +697,7 @@ func TestOneShot(t *testing.T) {
 			err := dockerClient.ConfigureByDSN(ts.dsn, labels, subLogger, "")
 			require.NoError(t, err)
 
-			dockerClient.Client = new(mockDockerCli)
+			dockerClient.Client = &mockDockerCli{}
 			out := make(chan types.Event, 100)
 			tomb := tomb.Tomb{}
 			err = dockerClient.OneShotAcquisition(ctx, out, &tomb)
diff --git a/pkg/setup/detect_test.go b/pkg/setup/detect_test.go
index 73dc9b957b8..d84a44aaa24 100644
--- a/pkg/setup/detect_test.go
+++ b/pkg/setup/detect_test.go
@@ -937,7 +937,7 @@ func TestDetectDatasourceValidation(t *testing.T) {
 				    datasource:
 				      source: docker`,
 			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
-			expectedErr: "invalid datasource for foobar: no containers names or containers ID configuration provided",
+			expectedErr: "invalid datasource for foobar: no containers or services configuration provided",
 		}, {
 			name: "source kinesis: required fields (enhanced fanout=false)",
 			config: `
diff --git a/test/bats/crowdsec-acquisition.bats b/test/bats/crowdsec-acquisition.bats
index 857b7b00372..a7c730b1f1b 100644
--- a/test/bats/crowdsec-acquisition.bats
+++ b/test/bats/crowdsec-acquisition.bats
@@ -116,6 +116,19 @@ teardown() {
     assert_stderr --partial "crowdsec init: while loading acquisition config: while configuring datasource of type docker from $ACQUIS_DIR/bad.yaml (position 0): while parsing DockerAcquisition configuration: [1:1] unknown field \\\"journalctl_filter\\\""
 }
 
+@test "datasource docker (regexp)" {
+    cat >"$ACQUIS_DIR"/bad.yaml <<-EOT
+	source: docker
+	container_name_regexp:
+	  - "[abc"
+	labels:
+	  type: syslog
+	EOT
+
+    rune -1 "$CROWDSEC" -t
+    assert_stderr --partial "crowdsec init: while loading acquisition config: while configuring datasource of type docker from $ACQUIS_DIR/bad.yaml (position 0): container_name_regexp: error parsing regexp: missing closing ]: \`[abc\`"
+}
+
 @test "test mode does not fail because of appsec and allowlists" {
     rune -0 cscli collections install crowdsecurity/appsec-virtual-patching
     cat >"$ACQUIS_DIR/appsec.yaml" <<-EOT