diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d9e94a7..cfb5f055 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ This document outlines major changes between releases.
 - Add `X-Amz-Content-Sha256` header validation (#218)
 - Support frostfsid contract. See `frostfsid` config section (#260)
 - Support per namespace placement policies configuration (see `namespaces.config` config param) (#266)
+- Support control api to manage policies. See `control` config section (#258)
 
 ### Changed
 - Update prometheus to v1.15.0 (#94)
diff --git a/Makefile b/Makefile
index 5822bcba..0c10a9b9 100755
--- a/Makefile
+++ b/Makefile
@@ -151,10 +151,16 @@ clean:
 
 # Generate code from .proto files
 protoc:
+	# Install specific version for protobuf lib
+	@GOBIN=$(abspath $(BINDIR)) go install -mod=mod -v git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/protogen
 	@for f in `find . -type f -name '*.proto' -not -path './vendor/*'`; do \
 		echo "⇒ Processing $$f "; \
 		protoc \
-			--go_out=paths=source_relative:. $$f; \
+			--go_out=paths=source_relative:. \
+            --plugin=protoc-gen-go-frostfs=$(BINDIR)/protogen \
+            --go-frostfs_out=. --go-frostfs_opt=paths=source_relative \
+            --go-grpc_opt=require_unimplemented_servers=false \
+            --go-grpc_out=. --go-grpc_opt=paths=source_relative $$f; \
 	done
 	rm -rf vendor
 
diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go
index 8bbf2922..b4182c65 100644
--- a/cmd/s3-gw/app.go
+++ b/cmd/s3-gw/app.go
@@ -4,8 +4,10 @@ import (
 	"context"
 	"encoding/hex"
 	"encoding/xml"
+	"errors"
 	"fmt"
 	"io"
+	"net"
 	"net/http"
 	"os"
 	"os/signal"
@@ -33,6 +35,8 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
+	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control"
+	controlSvc "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control/server"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
@@ -65,6 +69,8 @@ type (
 
 		servers []Server
 
+		controlAPI *grpc.Server
+
 		metrics        *metrics.AppMetrics
 		bucketResolver *resolver.BucketResolver
 		services       []*Service
@@ -91,6 +97,7 @@ type (
 		md5Enabled                    bool
 		namespaceHeader               string
 		defaultNamespaces             []string
+		authorizedControlAPIKeys      [][]byte
 	}
 
 	maxClientsConfig struct {
@@ -121,7 +128,7 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
 		webDone: make(chan struct{}, 1),
 		wrkDone: make(chan struct{}, 1),
 
-		settings: newAppSettings(log, v),
+		settings: newAppSettings(log, v, key),
 	}
 
 	app.init(ctx)
@@ -132,6 +139,7 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
 func (a *App) init(ctx context.Context) {
 	a.setRuntimeParameters()
 	a.initAPI(ctx)
+	a.initControlAPI()
 	a.initMetrics()
 	a.initFrostfsID(ctx)
 	a.initServers(ctx)
@@ -177,7 +185,7 @@ func (a *App) initLayer(ctx context.Context) {
 	}
 }
 
-func newAppSettings(log *Logger, v *viper.Viper) *appSettings {
+func newAppSettings(log *Logger, v *viper.Viper, key *keys.PrivateKey) *appSettings {
 	settings := &appSettings{
 		logLevel:           log.lvl,
 		maxClient:          newMaxClients(v),
@@ -199,6 +207,7 @@ func newAppSettings(log *Logger, v *viper.Viper) *appSettings {
 	settings.initPlacementPolicy(log.logger, v)
 	settings.setBufferMaxSizeForPut(v.GetUint64(cfgBufferMaxSizeForPut))
 	settings.setMD5Enabled(v.GetBool(cfgMD5Enabled))
+	settings.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log.logger, v), key.PublicKey()))
 
 	return settings
 }
@@ -355,11 +364,39 @@ func (s *appSettings) setDefaultNamespaces(namespaces []string) {
 	s.mu.Unlock()
 }
 
+func (s *appSettings) FetchRawKeys() [][]byte {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	return s.authorizedControlAPIKeys
+}
+
+func (s *appSettings) setAuthorizedControlAPIKeys(keys keys.PublicKeys) {
+	rawPubs := make([][]byte, len(keys))
+	for i := range keys {
+		rawPubs[i] = keys[i].Bytes()
+	}
+
+	s.mu.Lock()
+	s.authorizedControlAPIKeys = rawPubs
+	s.mu.Unlock()
+}
+
 func (a *App) initAPI(ctx context.Context) {
 	a.initLayer(ctx)
 	a.initHandler()
 }
 
+func (a *App) initControlAPI() {
+	svc := controlSvc.New(
+		controlSvc.WithAuthorizedKeysFetcher(a.settings),
+		controlSvc.WithLogger(a.log),
+	)
+
+	a.controlAPI = grpc.NewServer()
+
+	control.RegisterControlServiceServer(a.controlAPI, svc)
+}
+
 func (a *App) initMetrics() {
 	a.metrics = metrics.NewAppMetrics(a.log, frostfs.NewPoolStatistic(a.pool), a.cfg.GetBool(cfgPrometheusEnabled))
 	a.metrics.State().SetHealth(metrics.HealthStatusStarting)
@@ -608,6 +645,16 @@ func (a *App) Serve(ctx context.Context) {
 		}(i)
 	}
 
+	go func() {
+		address := a.cfg.GetString(cfgControlGRPCEndpoint)
+		a.log.Info(logs.StartingControlAPI, zap.String("address", address))
+		if listener, err := net.Listen("tcp", address); err != nil {
+			a.log.Fatal(logs.ListenAndServe, zap.Error(err))
+		} else if err = a.controlAPI.Serve(listener); err != nil {
+			a.log.Fatal(logs.ListenAndServe, zap.Error(err))
+		}
+	}()
+
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGHUP)
 
@@ -626,6 +673,7 @@ LOOP:
 
 	a.log.Info(logs.StoppingServer, zap.Error(srv.Shutdown(ctx)))
 
+	a.stopControlAPI()
 	a.metrics.Shutdown()
 	a.stopServices()
 	a.shutdownTracing()
@@ -637,6 +685,25 @@ func shutdownContext() (context.Context, context.CancelFunc) {
 	return context.WithTimeout(context.Background(), defaultShutdownTimeout)
 }
 
+func (a *App) stopControlAPI() {
+	ctx, cancel := shutdownContext()
+	defer cancel()
+
+	go func() {
+		a.controlAPI.GracefulStop()
+		cancel()
+	}()
+
+	<-ctx.Done()
+
+	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+		a.log.Info(logs.ControlAPICannotShutdownGracefully)
+		a.controlAPI.Stop()
+	}
+
+	a.log.Info(logs.ControlAPIServiceStopped)
+}
+
 func (a *App) configReload(ctx context.Context) {
 	a.log.Info(logs.SIGHUPConfigReloadStarted)
 
@@ -687,6 +754,7 @@ func (a *App) updateSettings() {
 	a.settings.setBufferMaxSizeForPut(a.cfg.GetUint64(cfgBufferMaxSizeForPut))
 	a.settings.setMD5Enabled(a.cfg.GetBool(cfgMD5Enabled))
 	a.settings.setDefaultNamespaces(a.cfg.GetStringSlice(cfgKludgeDefaultNamespaces))
+	a.settings.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(a.log, a.cfg), a.key.PublicKey()))
 }
 
 func (a *App) startServices() {
diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go
index 37ed8cc0..fe473764 100644
--- a/cmd/s3-gw/app_settings.go
+++ b/cmd/s3-gw/app_settings.go
@@ -21,6 +21,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
 	"git.frostfs.info/TrueCloudLab/zapjournald"
+	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
 	"github.com/ssgreg/journald"
@@ -79,6 +80,10 @@ const ( // Settings.
 	cfgTLSKeyFile  = "tls.key_file"
 	cfgTLSCertFile = "tls.cert_file"
 
+	// Control API.
+	cfgControlAuthorizedKeys = "control.authorized_keys"
+	cfgControlGRPCEndpoint   = "control.grpc.endpoint"
+
 	// Pool config.
 	cfgConnectTimeout     = "connect_timeout"
 	cfgStreamTimeout      = "stream_timeout"
@@ -585,6 +590,23 @@ func fetchServers(v *viper.Viper) []ServerInfo {
 	return servers
 }
 
+func fetchAuthorizedKeys(l *zap.Logger, v *viper.Viper) keys.PublicKeys {
+	strKeys := v.GetStringSlice(cfgControlAuthorizedKeys)
+	pubs := make(keys.PublicKeys, 0, len(strKeys))
+
+	for i := range strKeys {
+		pub, err := keys.NewPublicKeyFromString(strKeys[i])
+		if err != nil {
+			l.Warn(logs.FailedToParsePublicKey, zap.String("key", strKeys[i]))
+			continue
+		}
+
+		pubs = append(pubs, pub)
+	}
+
+	return pubs
+}
+
 func newSettings() *viper.Viper {
 	v := viper.New()
 
@@ -641,6 +663,8 @@ func newSettings() *viper.Viper {
 	v.SetDefault(cfgPProfAddress, "localhost:8085")
 	v.SetDefault(cfgPrometheusAddress, "localhost:8086")
 
+	v.SetDefault(cfgControlGRPCEndpoint, "localhost:8083")
+
 	// frostfs
 	v.SetDefault(cfgBufferMaxSizeForPut, 1024*1024) // 1mb
 
diff --git a/config/config.env b/config/config.env
index af305f47..d44d1680 100644
--- a/config/config.env
+++ b/config/config.env
@@ -33,6 +33,12 @@ S3_GW_SERVER_1_TLS_ENABLED=true
 S3_GW_SERVER_1_TLS_CERT_FILE=/path/to/tls/cert
 S3_GW_SERVER_1_TLS_KEY_FILE=/path/to/tls/key
 
+# Control API
+# List of hex-encoded public keys that have rights to use the Control Service
+S3_GW_CONTROL_AUTHORIZED_KEYS=035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6
+# Endpoint that is listened by the Control Service
+S3_GW_CONTROL_GRPC_ENDPOINT=localhost:8083
+
 # Domains to be able to use virtual-hosted-style access to bucket.
 S3_GW_LISTEN_DOMAINS=s3dev.frostfs.devenv
 
diff --git a/config/config.yaml b/config/config.yaml
index 7a4962eb..421d937c 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -37,6 +37,15 @@ server:
       cert_file: /path/to/cert
       key_file: /path/to/key
 
+control:
+  # List of hex-encoded public keys that have rights to use the Control Service
+  authorized_keys:
+    - 035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11
+    - 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6
+  grpc:
+    # Endpoint that is listened by the Control Service
+    endpoint: localhost:8083
+
 # Domains to be able to use virtual-hosted-style access to bucket.
 listen_domains:
   - s3dev.frostfs.devenv
diff --git a/docs/configuration.md b/docs/configuration.md
index e270540e..c9c0baee 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -175,6 +175,7 @@ There are some custom types used for brevity:
 | `peers`            | [Nodes configuration](#peers-section)                          |
 | `placement_policy` | [Placement policy configuration](#placement_policy-section)    |
 | `server`           | [Server configuration](#server-section)                        |
+| `control`          | [Control API configuration](#control-section)                  |
 | `logger`           | [Logger configuration](#logger-section)                        |
 | `cache`            | [Cache configuration](#cache-section)                          |
 | `nats`             | [NATS configuration](#nats-section)                            |
@@ -352,6 +353,24 @@ server:
 | `tls.cert_file` | `string` | yes           |                | Path to the TLS certificate.                  |
 | `tls.key_file`  | `string` | yes           |                | Path to the key.                              |
 
+### `control` section
+
+Control API parameters.
+
+```yaml
+control:
+  authorized_keys:  
+    - 035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11
+    - 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6
+  grpc:
+    endpoint: localhost:8083 
+```
+
+| Parameter         | Type       | SIGHUP reload | Default value    | Description                                                                  |
+|-------------------|------------|---------------|------------------|------------------------------------------------------------------------------|
+| `authorized_keys` | `[]string` | yes           |                  | List of hex-encoded public keys that have rights to use the Control Service. |
+| `grpc.endpoint`   | `string`   |               | `localhost:8083` | Endpoint that is listened by the Control Service.                            |
+
 ### `logger` section
 
 ```yaml
diff --git a/internal/logs/logs.go b/internal/logs/logs.go
index 0ad17d14..2113da7e 100644
--- a/internal/logs/logs.go
+++ b/internal/logs/logs.go
@@ -21,6 +21,7 @@ const (
 	ApplicationFinished                                  = "application finished"                                                                              // Info in ../../cmd/s3-gw/app.go
 	FetchDomainsPrepareToUseAPI                          = "fetch domains, prepare to use API"                                                                 // Info in ../../cmd/s3-gw/app.go
 	StartingServer                                       = "starting server"                                                                                   // Info in ../../cmd/s3-gw/app.go
+	StartingControlAPI                                   = "starting control API server"                                                                       // Info in ../../cmd/s3-gw/app.go
 	StoppingServer                                       = "stopping server"                                                                                   // Info in ../../cmd/s3-gw/app.go
 	SIGHUPConfigReloadStarted                            = "SIGHUP config reload started"                                                                      // Info in ../../cmd/s3-gw/app.go
 	FailedToReloadConfigBecauseItsMissed                 = "failed to reload config because it's missed"                                                       // Warn in ../../cmd/s3-gw/app.go
@@ -32,6 +33,8 @@ const (
 	FailedToAddServer                                    = "failed to add server"                                                                              // Warn in ../../cmd/s3-gw/app.go
 	AddServer                                            = "add server"                                                                                        // Info in ../../cmd/s3-gw/app.go
 	ResolverNNSWontBeUsedSinceRPCEndpointIsntProvided    = "resolver 'nns' won't be used since 'rpc_endpoint' isn't provided"                                  // Warn in ../../cmd/s3-gw/app.go
+	ControlAPICannotShutdownGracefully                   = "control API cannot shutdown gracefully, forcing stop"                                              // Info in ../../cmd/s3-gw/app.go
+	ControlAPIServiceStopped                             = "control API service stopped"                                                                       // Info in ../../cmd/s3-gw/app.go
 	InvalidLifetimeUsingDefaultValue                     = "invalid lifetime, using default value (in seconds)"                                                // Error in ../../cmd/s3-gw/app_settings.go
 	InvalidCacheSizeUsingDefaultValue                    = "invalid cache size, using default value"                                                           // Error in ../../cmd/s3-gw/app_settings.go
 	FailedToParseDefaultLocationConstraint               = "failed to parse 'default' location constraint, default one will be used"                           // Warn in cmd/s3-gw/app_settings.go
@@ -40,6 +43,7 @@ const (
 	FailedToParseLocationConstraint                      = "failed to parse location constraint, it cannot be used"                                            // Warn in cmd/s3-gw/app_settings.go
 	FailedToParseDefaultCopiesNumbers                    = "failed to parse 'default' copies numbers, default one will be used"                                // Warn in cmd/s3-gw/app_settings.go
 	FailedToParseCopiesNumbers                           = "failed to parse copies numbers, skip"                                                              // Warn in cmd/s3-gw/app_settings.go
+	FailedToParsePublicKey                               = "failed to parse public key, skip"                                                                  // Warn in cmd/s3-gw/app_settings.go
 	DefaultNamespacesCannotBeEmpty                       = "default namespaces cannot be empty, defaults will be used"                                         // Warn in cmd/s3-gw/app_settings.go
 	FailedToParseNamespacesConfig                        = "failed to unmarshal namespaces config"                                                             // Warn in cmd/s3-gw/app_settings.go
 	DefaultNamespaceConfigValuesBeOverwritten            = "default namespace config value be overwritten by values from 'namespaces.config'"                  // Warn in cmd/s3-gw/app_settings.go
diff --git a/pkg/service/control/client/client.go b/pkg/service/control/client/client.go
new file mode 100644
index 00000000..b87d5eae
--- /dev/null
+++ b/pkg/service/control/client/client.go
@@ -0,0 +1,56 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control"
+	controlSvc "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control/server"
+	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+	"go.uber.org/zap"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+type Client struct {
+	svc control.ControlServiceClient
+	key *keys.PrivateKey
+}
+
+type Config struct {
+	Logger *zap.Logger
+}
+
+func New(ctx context.Context, addr string, key *keys.PrivateKey) (*Client, error) {
+	conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		return nil, fmt.Errorf("failed to dial s3 gw control api: %w", err)
+	}
+
+	svc := control.NewControlServiceClient(conn)
+
+	cli := &Client{
+		svc: svc,
+		key: key,
+	}
+
+	return cli, cli.Healthcheck(ctx)
+}
+
+func (c *Client) Healthcheck(ctx context.Context) error {
+	req := &control.HealthCheckRequest{}
+	if err := controlSvc.SignMessage(&c.key.PrivateKey, req); err != nil {
+		return err
+	}
+
+	res, err := c.svc.HealthCheck(ctx, req)
+	if err != nil {
+		return err
+	}
+
+	if res.Body.HealthStatus != control.HealthStatus_READY {
+		return fmt.Errorf("service isn't ready, status: %s", res.Body.HealthStatus)
+	}
+
+	return nil
+}
diff --git a/pkg/service/control/server/server.go b/pkg/service/control/server/server.go
new file mode 100644
index 00000000..c45cf017
--- /dev/null
+++ b/pkg/service/control/server/server.go
@@ -0,0 +1,180 @@
+package server
+
+import (
+	"bytes"
+	"context"
+	"crypto/ecdsa"
+	"encoding/hex"
+	"errors"
+	"fmt"
+
+	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
+	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control"
+	frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
+	frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
+	"go.uber.org/zap"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+type Server struct {
+	*cfg
+}
+
+type AuthorizedKeysFetcher interface {
+	FetchRawKeys() [][]byte
+}
+
+type emptyKeysFetcher struct{}
+
+func (f emptyKeysFetcher) FetchRawKeys() [][]byte { return nil }
+
+// Option of the Server's constructor.
+type Option func(*cfg)
+
+type cfg struct {
+	log *zap.Logger
+
+	keysFetcher AuthorizedKeysFetcher
+}
+
+func defaultCfg() *cfg {
+	return &cfg{
+		log:         zap.NewNop(),
+		keysFetcher: emptyKeysFetcher{},
+	}
+}
+
+// New creates, initializes and returns new Server instance.
+func New(opts ...Option) *Server {
+	c := defaultCfg()
+
+	for _, opt := range opts {
+		opt(c)
+	}
+
+	c.log = c.log.With(zap.String("service", "control API"))
+
+	return &Server{
+		cfg: c,
+	}
+}
+
+// WithAuthorizedKeysFetcher returns option to add list of public
+// keys that have rights to use Control service.
+func WithAuthorizedKeysFetcher(fetcher AuthorizedKeysFetcher) Option {
+	return func(c *cfg) {
+		c.keysFetcher = fetcher
+	}
+}
+
+// WithLogger returns option to set logger.
+func WithLogger(log *zap.Logger) Option {
+	return func(c *cfg) {
+		c.log = log
+	}
+}
+
+// HealthCheck returns health status of the local node.
+//
+// If request is unsigned or signed by disallowed key, permission error returns.
+func (s *Server) HealthCheck(_ context.Context, req *control.HealthCheckRequest) (*control.HealthCheckResponse, error) {
+	s.log.Info("healthcheck", zap.String("key", hex.EncodeToString(req.Signature.Key)))
+
+	// verify request
+	if err := s.isValidRequest(req); err != nil {
+		return nil, status.Error(codes.PermissionDenied, err.Error())
+	}
+
+	resp := &control.HealthCheckResponse{
+		Body: &control.HealthCheckResponse_Body{
+			HealthStatus: control.HealthStatus_READY,
+		},
+	}
+
+	return resp, nil
+}
+
+// SignedMessage is an interface of Control service message.
+type SignedMessage interface {
+	ReadSignedData([]byte) ([]byte, error)
+	GetSignature() *control.Signature
+	SetSignature(*control.Signature)
+}
+
+var errDisallowedKey = errors.New("key is not in the allowed list")
+var errMissingSignature = errors.New("missing signature")
+var errInvalidSignature = errors.New("invalid signature")
+
+func (s *Server) isValidRequest(req SignedMessage) error {
+	sign := req.GetSignature()
+	if sign == nil {
+		return errMissingSignature
+	}
+
+	var (
+		key     = sign.GetKey()
+		allowed = false
+	)
+
+	// check if key is allowed
+	for _, authKey := range s.keysFetcher.FetchRawKeys() {
+		if allowed = bytes.Equal(authKey, key); allowed {
+			break
+		}
+	}
+
+	if !allowed {
+		return errDisallowedKey
+	}
+
+	// verify signature
+	binBody, err := req.ReadSignedData(nil)
+	if err != nil {
+		return fmt.Errorf("marshal request body: %w", err)
+	}
+
+	// TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion
+	var sigV2 refs.Signature
+	sigV2.SetKey(sign.GetKey())
+	sigV2.SetSign(sign.GetSign())
+	sigV2.SetScheme(refs.ECDSA_SHA512)
+
+	var sig frostfscrypto.Signature
+	if err := sig.ReadFromV2(sigV2); err != nil {
+		return fmt.Errorf("can't read signature: %w", err)
+	}
+
+	if !sig.Verify(binBody) {
+		return errInvalidSignature
+	}
+
+	return nil
+}
+
+// SignMessage signs Control service message with private key.
+func SignMessage(key *ecdsa.PrivateKey, msg SignedMessage) error {
+	binBody, err := msg.ReadSignedData(nil)
+	if err != nil {
+		return fmt.Errorf("marshal request body: %w", err)
+	}
+
+	var sig frostfscrypto.Signature
+
+	err = sig.Calculate(frostfsecdsa.Signer(*key), binBody)
+	if err != nil {
+		return fmt.Errorf("calculate signature: %w", err)
+	}
+
+	// TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion
+	var sigV2 refs.Signature
+	sig.WriteToV2(&sigV2)
+
+	var sigControl control.Signature
+	sigControl.Key = sigV2.GetKey()
+	sigControl.Sign = sigV2.GetSign()
+
+	msg.SetSignature(&sigControl)
+
+	return nil
+}
diff --git a/pkg/service/control/service.pb.go b/pkg/service/control/service.pb.go
new file mode 100644
index 00000000..3565cdf8
--- /dev/null
+++ b/pkg/service/control/service.pb.go
@@ -0,0 +1,514 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.26.0
+// 	protoc        v3.21.9
+// source: pkg/service/control/service.proto
+
+package control
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Health status of the storage node application.
+type HealthStatus int32
+
+const (
+	// Undefined status, default value.
+	HealthStatus_HEALTH_STATUS_UNDEFINED HealthStatus = 0
+	// Storage node application is starting.
+	HealthStatus_STARTING HealthStatus = 1
+	// Storage node application is started and serves all services.
+	HealthStatus_READY HealthStatus = 2
+	// Storage node application is shutting down.
+	HealthStatus_SHUTTING_DOWN HealthStatus = 3
+)
+
+// Enum value maps for HealthStatus.
+var (
+	HealthStatus_name = map[int32]string{
+		0: "HEALTH_STATUS_UNDEFINED",
+		1: "STARTING",
+		2: "READY",
+		3: "SHUTTING_DOWN",
+	}
+	HealthStatus_value = map[string]int32{
+		"HEALTH_STATUS_UNDEFINED": 0,
+		"STARTING":                1,
+		"READY":                   2,
+		"SHUTTING_DOWN":           3,
+	}
+)
+
+func (x HealthStatus) Enum() *HealthStatus {
+	p := new(HealthStatus)
+	*p = x
+	return p
+}
+
+func (x HealthStatus) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (HealthStatus) Descriptor() protoreflect.EnumDescriptor {
+	return file_pkg_service_control_service_proto_enumTypes[0].Descriptor()
+}
+
+func (HealthStatus) Type() protoreflect.EnumType {
+	return &file_pkg_service_control_service_proto_enumTypes[0]
+}
+
+func (x HealthStatus) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use HealthStatus.Descriptor instead.
+func (HealthStatus) EnumDescriptor() ([]byte, []int) {
+	return file_pkg_service_control_service_proto_rawDescGZIP(), []int{0}
+}
+
+// Signature of some message.
+type Signature struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Public key used for signing.
+	Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+	// Binary signature.
+	Sign []byte `protobuf:"bytes,2,opt,name=sign,json=signature,proto3" json:"sign,omitempty"`
+}
+
+func (x *Signature) Reset() {
+	*x = Signature{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_service_control_service_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Signature) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Signature) ProtoMessage() {}
+
+func (x *Signature) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_service_control_service_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Signature.ProtoReflect.Descriptor instead.
+func (*Signature) Descriptor() ([]byte, []int) {
+	return file_pkg_service_control_service_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Signature) GetKey() []byte {
+	if x != nil {
+		return x.Key
+	}
+	return nil
+}
+
+func (x *Signature) GetSign() []byte {
+	if x != nil {
+		return x.Sign
+	}
+	return nil
+}
+
+// Health check request.
+type HealthCheckRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Body of health check request message.
+	Body *HealthCheckRequest_Body `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"`
+	// Body signature.
+	Signature *Signature `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
+}
+
+func (x *HealthCheckRequest) Reset() {
+	*x = HealthCheckRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_service_control_service_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HealthCheckRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthCheckRequest) ProtoMessage() {}
+
+func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_service_control_service_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.
+func (*HealthCheckRequest) Descriptor() ([]byte, []int) {
+	return file_pkg_service_control_service_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *HealthCheckRequest) GetBody() *HealthCheckRequest_Body {
+	if x != nil {
+		return x.Body
+	}
+	return nil
+}
+
+func (x *HealthCheckRequest) GetSignature() *Signature {
+	if x != nil {
+		return x.Signature
+	}
+	return nil
+}
+
+// Health check response.
+type HealthCheckResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Body of health check response message.
+	Body      *HealthCheckResponse_Body `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"`
+	Signature *Signature                `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
+}
+
+func (x *HealthCheckResponse) Reset() {
+	*x = HealthCheckResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_service_control_service_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HealthCheckResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthCheckResponse) ProtoMessage() {}
+
+func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_service_control_service_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.
+func (*HealthCheckResponse) Descriptor() ([]byte, []int) {
+	return file_pkg_service_control_service_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *HealthCheckResponse) GetBody() *HealthCheckResponse_Body {
+	if x != nil {
+		return x.Body
+	}
+	return nil
+}
+
+func (x *HealthCheckResponse) GetSignature() *Signature {
+	if x != nil {
+		return x.Signature
+	}
+	return nil
+}
+
+type HealthCheckRequest_Body struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *HealthCheckRequest_Body) Reset() {
+	*x = HealthCheckRequest_Body{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_service_control_service_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HealthCheckRequest_Body) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthCheckRequest_Body) ProtoMessage() {}
+
+func (x *HealthCheckRequest_Body) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_service_control_service_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HealthCheckRequest_Body.ProtoReflect.Descriptor instead.
+func (*HealthCheckRequest_Body) Descriptor() ([]byte, []int) {
+	return file_pkg_service_control_service_proto_rawDescGZIP(), []int{1, 0}
+}
+
+// Health check response body
+type HealthCheckResponse_Body struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Health status of storage node application.
+	HealthStatus HealthStatus `protobuf:"varint,1,opt,name=health_status,json=healthStatus,proto3,enum=s3gw.control.HealthStatus" json:"health_status,omitempty"`
+}
+
+func (x *HealthCheckResponse_Body) Reset() {
+	*x = HealthCheckResponse_Body{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_service_control_service_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *HealthCheckResponse_Body) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthCheckResponse_Body) ProtoMessage() {}
+
+func (x *HealthCheckResponse_Body) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_service_control_service_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use HealthCheckResponse_Body.ProtoReflect.Descriptor instead.
+func (*HealthCheckResponse_Body) Descriptor() ([]byte, []int) {
+	return file_pkg_service_control_service_proto_rawDescGZIP(), []int{2, 0}
+}
+
+func (x *HealthCheckResponse_Body) GetHealthStatus() HealthStatus {
+	if x != nil {
+		return x.HealthStatus
+	}
+	return HealthStatus_HEALTH_STATUS_UNDEFINED
+}
+
+var File_pkg_service_control_service_proto protoreflect.FileDescriptor
+
+var file_pkg_service_control_service_proto_rawDesc = []byte{
+	0x0a, 0x21, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x63, 0x6f,
+	0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x73, 0x33, 0x67, 0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
+	0x6c, 0x22, 0x36, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x10,
+	0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79,
+	0x12, 0x17, 0x0a, 0x04, 0x73, 0x69, 0x67, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09,
+	0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x12, 0x48, 0x65,
+	0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x12, 0x39, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25,
+	0x2e, 0x73, 0x33, 0x67, 0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e, 0x48, 0x65,
+	0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x2e, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x35, 0x0a, 0x09, 0x73,
+	0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17,
+	0x2e, 0x73, 0x33, 0x67, 0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e, 0x53, 0x69,
+	0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
+	0x72, 0x65, 0x1a, 0x06, 0x0a, 0x04, 0x42, 0x6f, 0x64, 0x79, 0x22, 0xd1, 0x01, 0x0a, 0x13, 0x48,
+	0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x3a, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x26, 0x2e, 0x73, 0x33, 0x67, 0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e,
+	0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x2e, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x35,
+	0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x17, 0x2e, 0x73, 0x33, 0x67, 0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
+	0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e,
+	0x61, 0x74, 0x75, 0x72, 0x65, 0x1a, 0x47, 0x0a, 0x04, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x3f, 0x0a,
+	0x0d, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x73, 0x33, 0x67, 0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74,
+	0x72, 0x6f, 0x6c, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
+	0x52, 0x0c, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x57,
+	0x0a, 0x0c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b,
+	0x0a, 0x17, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f,
+	0x55, 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53,
+	0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41,
+	0x44, 0x59, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x48, 0x55, 0x54, 0x54, 0x49, 0x4e, 0x47,
+	0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x32, 0x64, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x72,
+	0x6f, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0b, 0x48, 0x65, 0x61,
+	0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x20, 0x2e, 0x73, 0x33, 0x67, 0x77, 0x2e,
+	0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68,
+	0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x73, 0x33, 0x67,
+	0x77, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68,
+	0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x41, 0x5a,
+	0x3f, 0x67, 0x69, 0x74, 0x2e, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2e, 0x69, 0x6e, 0x66,
+	0x6f, 0x2f, 0x54, 0x72, 0x75, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x4c, 0x61, 0x62, 0x2f, 0x66,
+	0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, 0x73, 0x33, 0x2d, 0x67, 0x77, 0x2f, 0x70, 0x6b, 0x67,
+	0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_pkg_service_control_service_proto_rawDescOnce sync.Once
+	file_pkg_service_control_service_proto_rawDescData = file_pkg_service_control_service_proto_rawDesc
+)
+
+func file_pkg_service_control_service_proto_rawDescGZIP() []byte {
+	file_pkg_service_control_service_proto_rawDescOnce.Do(func() {
+		file_pkg_service_control_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_service_control_service_proto_rawDescData)
+	})
+	return file_pkg_service_control_service_proto_rawDescData
+}
+
+var file_pkg_service_control_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_pkg_service_control_service_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_pkg_service_control_service_proto_goTypes = []interface{}{
+	(HealthStatus)(0),                // 0: s3gw.control.HealthStatus
+	(*Signature)(nil),                // 1: s3gw.control.Signature
+	(*HealthCheckRequest)(nil),       // 2: s3gw.control.HealthCheckRequest
+	(*HealthCheckResponse)(nil),      // 3: s3gw.control.HealthCheckResponse
+	(*HealthCheckRequest_Body)(nil),  // 4: s3gw.control.HealthCheckRequest.Body
+	(*HealthCheckResponse_Body)(nil), // 5: s3gw.control.HealthCheckResponse.Body
+}
+var file_pkg_service_control_service_proto_depIdxs = []int32{
+	4, // 0: s3gw.control.HealthCheckRequest.body:type_name -> s3gw.control.HealthCheckRequest.Body
+	1, // 1: s3gw.control.HealthCheckRequest.signature:type_name -> s3gw.control.Signature
+	5, // 2: s3gw.control.HealthCheckResponse.body:type_name -> s3gw.control.HealthCheckResponse.Body
+	1, // 3: s3gw.control.HealthCheckResponse.signature:type_name -> s3gw.control.Signature
+	0, // 4: s3gw.control.HealthCheckResponse.Body.health_status:type_name -> s3gw.control.HealthStatus
+	2, // 5: s3gw.control.ControlService.HealthCheck:input_type -> s3gw.control.HealthCheckRequest
+	3, // 6: s3gw.control.ControlService.HealthCheck:output_type -> s3gw.control.HealthCheckResponse
+	6, // [6:7] is the sub-list for method output_type
+	5, // [5:6] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_pkg_service_control_service_proto_init() }
+func file_pkg_service_control_service_proto_init() {
+	if File_pkg_service_control_service_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_pkg_service_control_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Signature); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_service_control_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HealthCheckRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_service_control_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HealthCheckResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_service_control_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HealthCheckRequest_Body); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_service_control_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*HealthCheckResponse_Body); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_pkg_service_control_service_proto_rawDesc,
+			NumEnums:      1,
+			NumMessages:   5,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_pkg_service_control_service_proto_goTypes,
+		DependencyIndexes: file_pkg_service_control_service_proto_depIdxs,
+		EnumInfos:         file_pkg_service_control_service_proto_enumTypes,
+		MessageInfos:      file_pkg_service_control_service_proto_msgTypes,
+	}.Build()
+	File_pkg_service_control_service_proto = out.File
+	file_pkg_service_control_service_proto_rawDesc = nil
+	file_pkg_service_control_service_proto_goTypes = nil
+	file_pkg_service_control_service_proto_depIdxs = nil
+}
diff --git a/pkg/service/control/service.proto b/pkg/service/control/service.proto
new file mode 100644
index 00000000..a48b1eb7
--- /dev/null
+++ b/pkg/service/control/service.proto
@@ -0,0 +1,62 @@
+syntax = "proto3";
+
+package s3gw.control;
+
+option go_package = "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control";
+
+// `ControlService` provides an interface for internal work with the storage node.
+service ControlService {
+  // Performs health check of the storage node.
+  rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse);
+}
+
+// Signature of some message.
+message Signature {
+  // Public key used for signing.
+  bytes key = 1 [json_name = "key"];
+
+  // Binary signature.
+  bytes sign = 2 [json_name = "signature"];
+}
+
+// Health check request.
+message HealthCheckRequest {
+  message Body {
+  }
+
+  // Body of health check request message.
+  Body body = 1;
+
+  // Body signature.
+  Signature signature = 2;
+}
+
+// Health check response.
+message HealthCheckResponse {
+  // Health check response body
+  message Body {
+    // Health status of storage node application.
+    HealthStatus health_status = 1;
+  }
+
+  // Body of health check response message.
+  Body body = 1;
+
+  Signature signature = 2;
+}
+
+
+// Health status of the storage node application.
+enum HealthStatus {
+  // Undefined status, default value.
+  HEALTH_STATUS_UNDEFINED = 0;
+
+  // Storage node application is starting.
+  STARTING = 1;
+
+  // Storage node application is started and serves all services.
+  READY = 2;
+
+  // Storage node application is shutting down.
+  SHUTTING_DOWN = 3;
+}
\ No newline at end of file
diff --git a/pkg/service/control/service_frostfs.pb.go b/pkg/service/control/service_frostfs.pb.go
new file mode 100644
index 00000000..4a196499
--- /dev/null
+++ b/pkg/service/control/service_frostfs.pb.go
@@ -0,0 +1,201 @@
+// Code generated by protoc-gen-go-frostfs. DO NOT EDIT.
+
+package control
+
+import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/proto"
+
+// StableSize returns the size of x in protobuf format.
+//
+// Structures with the same field values have the same binary size.
+func (x *Signature) StableSize() (size int) {
+	if x == nil {
+		return 0
+	}
+	size += proto.BytesSize(1, x.Key)
+	size += proto.BytesSize(2, x.Sign)
+	return size
+}
+
+// StableMarshal marshals x in protobuf binary format with stable field order.
+//
+// If buffer length is less than x.StableSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same binary format.
+func (x *Signature) StableMarshal(buf []byte) []byte {
+	if x == nil {
+		return []byte{}
+	}
+	if buf == nil {
+		buf = make([]byte, x.StableSize())
+	}
+	var offset int
+	offset += proto.BytesMarshal(1, buf[offset:], x.Key)
+	offset += proto.BytesMarshal(2, buf[offset:], x.Sign)
+	return buf
+}
+
+// StableSize returns the size of x in protobuf format.
+//
+// Structures with the same field values have the same binary size.
+func (x *HealthCheckRequest_Body) StableSize() (size int) {
+	if x == nil {
+		return 0
+	}
+	return size
+}
+
+// StableMarshal marshals x in protobuf binary format with stable field order.
+//
+// If buffer length is less than x.StableSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same binary format.
+func (x *HealthCheckRequest_Body) StableMarshal(buf []byte) []byte {
+	return buf
+}
+
+// StableSize returns the size of x in protobuf format.
+//
+// Structures with the same field values have the same binary size.
+func (x *HealthCheckRequest) StableSize() (size int) {
+	if x == nil {
+		return 0
+	}
+	size += proto.NestedStructureSize(1, x.Body)
+	size += proto.NestedStructureSize(2, x.Signature)
+	return size
+}
+
+// StableMarshal marshals x in protobuf binary format with stable field order.
+//
+// If buffer length is less than x.StableSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same binary format.
+func (x *HealthCheckRequest) StableMarshal(buf []byte) []byte {
+	if x == nil {
+		return []byte{}
+	}
+	if buf == nil {
+		buf = make([]byte, x.StableSize())
+	}
+	var offset int
+	offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
+	offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
+	return buf
+}
+
+// ReadSignedData fills buf with signed data of x.
+// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same signed data.
+func (x *HealthCheckRequest) SignedDataSize() int {
+	return x.GetBody().StableSize()
+}
+
+// SignedDataSize returns size of the request signed data in bytes.
+//
+// Structures with the same field values have the same signed data size.
+func (x *HealthCheckRequest) ReadSignedData(buf []byte) ([]byte, error) {
+	return x.GetBody().StableMarshal(buf), nil
+}
+
+func (x *HealthCheckRequest) SetSignature(sig *Signature) {
+	x.Signature = sig
+}
+
+// StableSize returns the size of x in protobuf format.
+//
+// Structures with the same field values have the same binary size.
+func (x *HealthCheckResponse_Body) StableSize() (size int) {
+	if x == nil {
+		return 0
+	}
+	size += proto.EnumSize(1, int32(x.HealthStatus))
+	return size
+}
+
+// StableMarshal marshals x in protobuf binary format with stable field order.
+//
+// If buffer length is less than x.StableSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same binary format.
+func (x *HealthCheckResponse_Body) StableMarshal(buf []byte) []byte {
+	if x == nil {
+		return []byte{}
+	}
+	if buf == nil {
+		buf = make([]byte, x.StableSize())
+	}
+	var offset int
+	offset += proto.EnumMarshal(1, buf[offset:], int32(x.HealthStatus))
+	return buf
+}
+
+// StableSize returns the size of x in protobuf format.
+//
+// Structures with the same field values have the same binary size.
+func (x *HealthCheckResponse) StableSize() (size int) {
+	if x == nil {
+		return 0
+	}
+	size += proto.NestedStructureSize(1, x.Body)
+	size += proto.NestedStructureSize(2, x.Signature)
+	return size
+}
+
+// StableMarshal marshals x in protobuf binary format with stable field order.
+//
+// If buffer length is less than x.StableSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same binary format.
+func (x *HealthCheckResponse) StableMarshal(buf []byte) []byte {
+	if x == nil {
+		return []byte{}
+	}
+	if buf == nil {
+		buf = make([]byte, x.StableSize())
+	}
+	var offset int
+	offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
+	offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
+	return buf
+}
+
+// ReadSignedData fills buf with signed data of x.
+// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
+//
+// Returns any error encountered which did not allow writing the data completely.
+// Otherwise, returns the buffer in which the data is written.
+//
+// Structures with the same field values have the same signed data.
+func (x *HealthCheckResponse) SignedDataSize() int {
+	return x.GetBody().StableSize()
+}
+
+// SignedDataSize returns size of the request signed data in bytes.
+//
+// Structures with the same field values have the same signed data size.
+func (x *HealthCheckResponse) ReadSignedData(buf []byte) ([]byte, error) {
+	return x.GetBody().StableMarshal(buf), nil
+}
+
+func (x *HealthCheckResponse) SetSignature(sig *Signature) {
+	x.Signature = sig
+}
diff --git a/pkg/service/control/service_grpc.pb.go b/pkg/service/control/service_grpc.pb.go
new file mode 100644
index 00000000..fc685207
--- /dev/null
+++ b/pkg/service/control/service_grpc.pb.go
@@ -0,0 +1,105 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.2.0
+// - protoc             v3.21.9
+// source: pkg/service/control/service.proto
+
+package control
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// ControlServiceClient is the client API for ControlService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ControlServiceClient interface {
+	// Performs health check of the storage node.
+	HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
+}
+
+type controlServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewControlServiceClient(cc grpc.ClientConnInterface) ControlServiceClient {
+	return &controlServiceClient{cc}
+}
+
+func (c *controlServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
+	out := new(HealthCheckResponse)
+	err := c.cc.Invoke(ctx, "/s3gw.control.ControlService/HealthCheck", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// ControlServiceServer is the server API for ControlService service.
+// All implementations should embed UnimplementedControlServiceServer
+// for forward compatibility
+type ControlServiceServer interface {
+	// Performs health check of the storage node.
+	HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
+}
+
+// UnimplementedControlServiceServer should be embedded to have forward compatible implementations.
+type UnimplementedControlServiceServer struct {
+}
+
+func (UnimplementedControlServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method HealthCheck not implemented")
+}
+
+// UnsafeControlServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ControlServiceServer will
+// result in compilation errors.
+type UnsafeControlServiceServer interface {
+	mustEmbedUnimplementedControlServiceServer()
+}
+
+func RegisterControlServiceServer(s grpc.ServiceRegistrar, srv ControlServiceServer) {
+	s.RegisterService(&ControlService_ServiceDesc, srv)
+}
+
+func _ControlService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(HealthCheckRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ControlServiceServer).HealthCheck(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/s3gw.control.ControlService/HealthCheck",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ControlServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// ControlService_ServiceDesc is the grpc.ServiceDesc for ControlService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ControlService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "s3gw.control.ControlService",
+	HandlerType: (*ControlServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "HealthCheck",
+			Handler:    _ControlService_HealthCheck_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "pkg/service/control/service.proto",
+}