diff --git a/cmd/neofs-cli/modules/acl.go b/cmd/neofs-cli/modules/acl.go
deleted file mode 100644
index 101e48ec7..000000000
--- a/cmd/neofs-cli/modules/acl.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-)
-
-// aclCmd represents the acl command
-var aclCmd = &cobra.Command{
-	Use:   "acl",
-	Short: "Operations with Access Control Lists",
-	Long:  `Operations with Access Control Lists`,
-	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("acl called")
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(aclCmd)
-
-	// Here you will define your flags and configuration settings.
-
-	// Cobra supports Persistent Flags which will work for this command
-	// and all subcommands, e.g.:
-	// aclCmd.PersistentFlags().String("foo", "", "A help for foo")
-
-	// Cobra supports local flags which will only run when this command
-	// is called directly, e.g.:
-	// aclCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
-}
diff --git a/cmd/neofs-cli/modules/acl/extended/create.go b/cmd/neofs-cli/modules/acl/extended/create.go
new file mode 100644
index 000000000..8d6aa40d5
--- /dev/null
+++ b/cmd/neofs-cli/modules/acl/extended/create.go
@@ -0,0 +1,275 @@
+package extended
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"strings"
+
+	"github.com/flynn-archive/go-shlex"
+	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
+	"github.com/nspcc-dev/neofs-sdk-go/eacl"
+	"github.com/spf13/cobra"
+)
+
+var createCmd = &cobra.Command{
+	Use:   "create",
+	Short: "Create extended ACL from the text representation",
+	Long: `Create extended ACL from the text representation.
+
+Rule consist of these blocks: <action> <operation> [<filter1> ...] [<target1> ...]
+
+Action is 'allow' or 'deny'.
+
+Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'.
+
+Filter consists of <typ>:<key><match><value>
+  Typ is 'obj' for object applied filter or 'req' for request applied filter. 
+  Key is a valid unicode string corresponding to object or request header key.
+  Match is '==' for matching and '!=' for non-matching filter.
+  Value is a valid unicode string corresponding to object or request header value.
+
+Target is 
+  'user' for container owner, 
+  'system' for Storage nodes in container and Inner Ring nodes,
+  'others' for all other request senders, 
+  'pubkey:<key1>,<key2>,...' for exact request sender, where <key> is a hex-encoded 33-byte public key.
+
+When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting extended ACL table.
+`,
+	Example: `neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -f rules.txt --out table.json
+neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others'`,
+	Run: createEACL,
+}
+
+func init() {
+	createCmd.Flags().StringArrayP("rule", "r", nil, "extended ACL table record to apply")
+	createCmd.Flags().StringP("file", "f", "", "read list of extended ACL table records from from text file")
+	createCmd.Flags().StringP("out", "o", "", "save JSON formatted extended ACL table in file")
+	createCmd.Flags().StringP("cid", "", "", "container ID")
+
+	_ = cobra.MarkFlagFilename(createCmd.Flags(), "file")
+	_ = cobra.MarkFlagFilename(createCmd.Flags(), "out")
+}
+
+func createEACL(cmd *cobra.Command, _ []string) {
+	rules, _ := cmd.Flags().GetStringArray("rule")
+	fileArg, _ := cmd.Flags().GetString("file")
+	outArg, _ := cmd.Flags().GetString("out")
+	cidArg, _ := cmd.Flags().GetString("cid")
+
+	containerID := cid.New()
+	if err := containerID.Parse(cidArg); err != nil {
+		cmd.Printf("invalid container ID: %v", err)
+		return
+	}
+
+	rulesFile, err := getRulesFromFile(fileArg)
+	if err != nil {
+		cmd.Printf("can't read rules from file : %v", err)
+		return
+	}
+
+	rules = append(rules, rulesFile...)
+	if len(rules) == 0 {
+		cmd.Println("no extended ACL rules has been provided")
+		return
+	}
+
+	tb := eacl.NewTable()
+
+	for _, ruleStr := range rules {
+		r, err := shlex.Split(ruleStr)
+		if err != nil {
+			cmd.Printf("can't parse rule '%s': %v)", ruleStr, err)
+			return
+		}
+
+		err = parseTable(tb, r)
+		if err != nil {
+			cmd.Printf("can't create extended ACL record from rule '%s': %v", ruleStr, err)
+			return
+		}
+	}
+
+	tb.SetCID(containerID)
+
+	data, err := tb.MarshalJSON()
+	if err != nil {
+		cmd.Println(err)
+		return
+	}
+
+	buf := new(bytes.Buffer)
+	err = json.Indent(buf, data, "", "  ")
+	if err != nil {
+		cmd.Println(err)
+		return
+	}
+
+	if len(outArg) == 0 {
+		cmd.Println(buf)
+		return
+	}
+
+	err = ioutil.WriteFile(outArg, buf.Bytes(), 0644)
+	if err != nil {
+		cmd.Println(err)
+	}
+}
+
+func getRulesFromFile(filename string) ([]string, error) {
+	if len(filename) == 0 {
+		return nil, nil
+	}
+
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	return strings.Split(strings.TrimSpace(string(data)), "\n"), nil
+}
+
+// parseTable parses eACL table from the following form:
+// <action> <operation> [<filter1> ...] [<target1> ...]
+//
+// Examples:
+// allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4
+func parseTable(tb *eacl.Table, args []string) error {
+	if len(args) < 2 {
+		return errors.New("at least 2 arguments must be provided")
+	}
+
+	var action eacl.Action
+	if !action.FromString(strings.ToUpper(args[0])) {
+		return errors.New("invalid action (expected 'allow' or 'deny')")
+	}
+
+	ops, err := parseOperations(args[1])
+	if err != nil {
+		return err
+	}
+
+	r, err := parseRecord(args[2:])
+	if err != nil {
+		return err
+	}
+
+	r.SetAction(action)
+
+	for _, op := range ops {
+		r := *r
+		r.SetOperation(op)
+		tb.AddRecord(&r)
+	}
+
+	return nil
+}
+
+func parseRecord(args []string) (*eacl.Record, error) {
+	r := new(eacl.Record)
+	for i := range args {
+		ss := strings.SplitN(args[i], ":", 2)
+
+		switch prefix := strings.ToLower(ss[0]); prefix {
+		case "req", "obj": // filters
+			if len(ss) != 2 {
+				return nil, fmt.Errorf("invalid filter or target: %s", args[i])
+			}
+
+			i := strings.Index(ss[1], "=")
+			if i < 0 {
+				return nil, fmt.Errorf("invalid filter key-value pair: %s", ss[1])
+			}
+
+			var key, value string
+			var op eacl.Match
+
+			if 0 < i && ss[1][i-1] == '!' {
+				key = ss[1][:i-1]
+				op = eacl.MatchStringNotEqual
+			} else {
+				key = ss[1][:i]
+				op = eacl.MatchStringEqual
+			}
+
+			value = ss[1][i+1:]
+
+			typ := eacl.HeaderFromRequest
+			if ss[0] == "obj" {
+				typ = eacl.HeaderFromObject
+			}
+
+			r.AddFilter(typ, op, key, value)
+		case "others", "system", "user", "pubkey": // targets
+			var err error
+
+			var pubs []ecdsa.PublicKey
+			if len(ss) == 2 {
+				pubs, err = parseKeyList(ss[1])
+				if err != nil {
+					return nil, err
+				}
+			}
+
+			var role eacl.Role
+			if prefix != "pubkey" {
+				role, err = roleFromString(prefix)
+				if err != nil {
+					return nil, err
+				}
+			}
+
+			eacl.AddFormedTarget(r, role, pubs...)
+
+		default:
+			return nil, fmt.Errorf("invalid prefix: %s", ss[0])
+		}
+	}
+
+	return r, nil
+}
+
+func roleFromString(s string) (eacl.Role, error) {
+	var r eacl.Role
+	if !r.FromString(strings.ToUpper(s)) {
+		return r, fmt.Errorf("unexpected role %s", s)
+	}
+
+	return r, nil
+}
+
+// parseKeyList parses list of hex-encoded public keys separated by comma.
+func parseKeyList(s string) ([]ecdsa.PublicKey, error) {
+	ss := strings.Split(s, ",")
+	pubs := make([]ecdsa.PublicKey, len(ss))
+	for i := range ss {
+		st := strings.TrimPrefix(ss[i], "0x")
+		pub, err := keys.NewPublicKeyFromString(st)
+		if err != nil {
+			return nil, fmt.Errorf("invalid public key '%s': %w", ss[i], err)
+		}
+
+		pubs[i] = ecdsa.PublicKey(*pub)
+	}
+
+	return pubs, nil
+}
+
+func parseOperations(s string) ([]eacl.Operation, error) {
+	ss := strings.Split(s, ",")
+	ops := make([]eacl.Operation, len(ss))
+
+	for i := range ss {
+		if !ops[i].FromString(strings.ToUpper(ss[i])) {
+			return nil, fmt.Errorf("invalid operation: %s", ss[i])
+		}
+	}
+
+	return ops, nil
+}
diff --git a/cmd/neofs-cli/modules/acl/extended/create_test.go b/cmd/neofs-cli/modules/acl/extended/create_test.go
new file mode 100644
index 000000000..88da39533
--- /dev/null
+++ b/cmd/neofs-cli/modules/acl/extended/create_test.go
@@ -0,0 +1,91 @@
+package extended
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/nspcc-dev/neofs-sdk-go/eacl"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseTable(t *testing.T) {
+	tests := [...]struct {
+		name       string // test name
+		rule       string // input extended ACL rule
+		jsonRecord string // produced record after successfull parsing
+	}{
+		{
+			name:       "valid rule with multiple filters",
+			rule:       "deny get obj:a=b req:c=d others",
+			jsonRecord: `{"operation":"GET","action":"DENY","filters":[{"headerType":"OBJECT","matchType":"STRING_EQUAL","key":"a","value":"b"},{"headerType":"REQUEST","matchType":"STRING_EQUAL","key":"c","value":"d"}],"targets":[{"role":"OTHERS","keys":[]}]}`,
+		},
+		{
+			name:       "valid rule without filters",
+			rule:       "allow put user",
+			jsonRecord: `{"operation":"PUT","action":"ALLOW","filters":[],"targets":[{"role":"USER","keys":[]}]}`,
+		},
+		{
+			name:       "valid rule with public key",
+			rule:       "deny getrange pubkey:036410abb260bbbda89f61c0cad65a4fa15ac5cb83b3c3abf8aee403856fcf65ed",
+			jsonRecord: `{"operation":"GETRANGE","action":"DENY","filters":[],"targets":[{"role":"ROLE_UNSPECIFIED","keys":["A2QQq7Jgu72on2HAytZaT6FaxcuDs8Or+K7kA4Vvz2Xt"]}]}`,
+		},
+		{
+			name: "missing action",
+			rule: "get obj:a=b others",
+		},
+		{
+			name: "invalid action",
+			rule: "permit get obj:a=b others",
+		},
+		{
+			name: "missing op",
+			rule: "deny obj:a=b others",
+		},
+		{
+			name: "invalid op action",
+			rule: "deny look obj:a=b others",
+		},
+		{
+			name: "invalid filter type",
+			rule: "deny get invalid:a=b others",
+		},
+		{
+			name: "invalid target group",
+			rule: "deny get obj:a=b helpers",
+		},
+		{
+			name: "invalid public key",
+			rule: "deny get obj:a=b pubkey:0123",
+		},
+	}
+
+	eaclTable := eacl.NewTable()
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			ss := strings.Split(test.rule, " ")
+			err := parseTable(eaclTable, ss)
+			ok := len(test.jsonRecord) > 0
+			require.Equal(t, ok, err == nil, err)
+			if ok {
+				expectedRecord := eacl.NewRecord()
+				err = expectedRecord.UnmarshalJSON([]byte(test.jsonRecord))
+				require.NoError(t, err)
+
+				actualRecord := eaclTable.Records()[len(eaclTable.Records())-1]
+
+				equalRecords(t, expectedRecord, actualRecord)
+			}
+		})
+	}
+}
+
+func equalRecords(t *testing.T, r1, r2 *eacl.Record) {
+	d1, err := r1.Marshal()
+	require.NoError(t, err)
+
+	d2, err := r2.Marshal()
+	require.NoError(t, err)
+
+	require.Equal(t, d1, d2)
+}
diff --git a/cmd/neofs-cli/modules/acl/extended/root.go b/cmd/neofs-cli/modules/acl/extended/root.go
new file mode 100644
index 000000000..673652549
--- /dev/null
+++ b/cmd/neofs-cli/modules/acl/extended/root.go
@@ -0,0 +1,12 @@
+package extended
+
+import "github.com/spf13/cobra"
+
+var Cmd = &cobra.Command{
+	Use:   "extended",
+	Short: "Operations with Extended Access Control Lists",
+}
+
+func init() {
+	Cmd.AddCommand(createCmd)
+}
diff --git a/cmd/neofs-cli/modules/acl/root.go b/cmd/neofs-cli/modules/acl/root.go
new file mode 100644
index 000000000..0fc3ecea3
--- /dev/null
+++ b/cmd/neofs-cli/modules/acl/root.go
@@ -0,0 +1,15 @@
+package acl
+
+import (
+	"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl/extended"
+	"github.com/spf13/cobra"
+)
+
+var Cmd = &cobra.Command{
+	Use:   "acl",
+	Short: "Operations with Access Control Lists",
+}
+
+func init() {
+	Cmd.AddCommand(extended.Cmd)
+}
diff --git a/cmd/neofs-cli/modules/root.go b/cmd/neofs-cli/modules/root.go
index e33746305..fed23d573 100644
--- a/cmd/neofs-cli/modules/root.go
+++ b/cmd/neofs-cli/modules/root.go
@@ -15,6 +15,7 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neo-go/pkg/wallet"
+	"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl"
 	"github.com/nspcc-dev/neofs-node/misc"
 	"github.com/nspcc-dev/neofs-node/pkg/network"
 	"github.com/nspcc-dev/neofs-sdk-go/client"
@@ -120,6 +121,8 @@ func init() {
 	// Cobra also supports local flags, which will only run
 	// when this action is called directly.
 	rootCmd.Flags().Bool("version", false, "Application version and NeoFS API compatibility")
+
+	rootCmd.AddCommand(acl.Cmd)
 }
 
 func entryPoint(cmd *cobra.Command, _ []string) {
diff --git a/go.mod b/go.mod
index 26acff9be..cad1d7039 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/nspcc-dev/neofs-node
 go 1.16
 
 require (
+	github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
 	github.com/google/go-github/v39 v39.2.0
 	github.com/google/uuid v1.2.0
 	github.com/hashicorp/golang-lru v0.5.4
diff --git a/go.sum b/go.sum
index 9d7820678..cb1ccb2d2 100644
Binary files a/go.sum and b/go.sum differ