diff --git a/cmd/neofs-adm/internal/modules/morph/group.go b/cmd/neofs-adm/internal/modules/morph/group.go
new file mode 100644
index 0000000000..854aa61e76
--- /dev/null
+++ b/cmd/neofs-adm/internal/modules/morph/group.go
@@ -0,0 +1,73 @@
+package morph
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+
+	"github.com/nspcc-dev/neo-go/cli/input"
+	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neo-go/pkg/wallet"
+	"github.com/spf13/viper"
+)
+
+const contractWalletName = "contract.json"
+
+func openContractWallet(walletDir string) (*wallet.Wallet, error) {
+	p := path.Join(walletDir, contractWalletName)
+	w, err := wallet.NewWalletFromFile(p)
+	if err != nil {
+		return nil, fmt.Errorf("can't open wallet: %w", err)
+	}
+
+	var password string
+	if key := "credentials.contract"; viper.IsSet(key) {
+		password = viper.GetString(key)
+	} else {
+		prompt := "Password for contract wallet > "
+		password, err = input.ReadPassword(prompt)
+		if err != nil {
+			return nil, fmt.Errorf("can't fetch password: %w", err)
+		}
+	}
+
+	for i := range w.Accounts {
+		if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
+			return nil, fmt.Errorf("can't unlock wallet: %w", err)
+		}
+	}
+
+	return w, nil
+}
+
+func (c *initializeContext) addManifestGroup(h util.Uint160, cs *contractState) error {
+	priv := c.ContractWallet.Accounts[0].PrivateKey()
+	pub := priv.PublicKey()
+
+	sig := priv.Sign(h.BytesBE())
+	found := false
+
+	for i := range cs.Manifest.Groups {
+		if cs.Manifest.Groups[i].PublicKey.Equal(pub) {
+			cs.Manifest.Groups[i].Signature = sig
+			found = true
+			break
+		}
+	}
+	if !found {
+		cs.Manifest.Groups = append(cs.Manifest.Groups, manifest.Group{
+			PublicKey: pub,
+			Signature: sig,
+		})
+	}
+
+	data, err := json.Marshal(cs.Manifest)
+	if err != nil {
+		return err
+	}
+
+	cs.RawManifest = data
+	return nil
+}
diff --git a/cmd/neofs-adm/internal/modules/morph/initialize.go b/cmd/neofs-adm/internal/modules/morph/initialize.go
index 04accf7f03..472322038e 100644
--- a/cmd/neofs-adm/internal/modules/morph/initialize.go
+++ b/cmd/neofs-adm/internal/modules/morph/initialize.go
@@ -28,6 +28,8 @@ type initializeContext struct {
 	// ConsensusAcc is used for retrieving committee address and verification script.
 	ConsensusAcc *wallet.Account
 	Wallets      []*wallet.Wallet
+	// ContractWallet is a wallet for providing contract group signature.
+	ContractWallet *wallet.Wallet
 	// Accounts contains simple signature accounts in the same order as in Wallets.
 	Accounts     []*wallet.Account
 	Contracts    map[string]*contractState
@@ -95,6 +97,11 @@ func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContex
 		return nil, err
 	}
 
+	w, err := openContractWallet(walletDir)
+	if err != nil {
+		return nil, err
+	}
+
 	c, err := getN3Client(v)
 	if err != nil {
 		return nil, fmt.Errorf("can't create N3 client: %w", err)
@@ -144,15 +151,16 @@ func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContex
 	}
 
 	initCtx := &initializeContext{
-		clientContext: *defaultClientContext(c),
-		ConsensusAcc:  consensusAcc,
-		CommitteeAcc:  committeeAcc,
-		Wallets:       wallets,
-		Accounts:      accounts,
-		Command:       cmd,
-		Contracts:     make(map[string]*contractState),
-		ContractPath:  ctrPath,
-		Natives:       nativeHashes,
+		clientContext:  *defaultClientContext(c),
+		ConsensusAcc:   consensusAcc,
+		CommitteeAcc:   committeeAcc,
+		ContractWallet: w,
+		Wallets:        wallets,
+		Accounts:       accounts,
+		Command:        cmd,
+		Contracts:      make(map[string]*contractState),
+		ContractPath:   ctrPath,
+		Natives:        nativeHashes,
 	}
 
 	if needContracts {
diff --git a/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go b/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go
index c994a929db..529a45b498 100644
--- a/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go
+++ b/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go
@@ -96,11 +96,20 @@ const (
 
 func (c *initializeContext) deployNNS(method string) error {
 	cs := c.getContract(nnsContract)
+	h := cs.Hash
 
 	realCs, err := c.Client.GetContractStateByID(1)
-	if err == nil && realCs.NEF.Checksum == cs.NEF.Checksum {
-		c.Command.Println("NNS contract is already deployed.")
-		return nil
+	if err == nil {
+		if realCs.NEF.Checksum == cs.NEF.Checksum {
+			c.Command.Println("NNS contract is already deployed.")
+			return nil
+		}
+		h = realCs.Hash
+	}
+
+	err = c.addManifestGroup(h, cs)
+	if err != nil {
+		return fmt.Errorf("can't sign manifest group: %v", err)
 	}
 
 	params := getContractDeployParameters(cs.RawNEF, cs.RawManifest, nil)
@@ -220,6 +229,11 @@ func (c *initializeContext) updateContracts() error {
 			}
 		}
 
+		err = c.addManifestGroup(ctrHash, alphaCs)
+		if err != nil {
+			return fmt.Errorf("can't sign manifest group: %v", err)
+		}
+
 		invokeHash := mgmtHash
 		if method == updateMethodName {
 			invokeHash = ctrHash
@@ -322,6 +336,11 @@ func (c *initializeContext) deployContracts() error {
 			continue
 		}
 
+		err := c.addManifestGroup(ctrHash, cs)
+		if err != nil {
+			return fmt.Errorf("can't sign manifest group: %v", err)
+		}
+
 		invokeHash := mgmtHash
 		params := getContractDeployParameters(cs.RawNEF, cs.RawManifest,
 			c.getContractDeployData(ctrName, keysParam))