├── .gitignore ├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── config.go ├── config_sample.yaml ├── config_test.go ├── go.mod ├── go.sum ├── main.go ├── syncgroup.go └── syncgroup_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | vendor/ 9 | sgsync* 10 | .vscode 11 | config_prod.yaml 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Oğuzhan YILMAZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Security Group Synchronization Tool 2 | 3 | Of course, you can easily do these things using AWS's VPC Peering feature. This is the best practice. This tool includes a complete dirty-hack. 4 | 5 | This tool monitors the resource AWS Security Group that you specify and synchronizes it to multiple Security Groups that you specify. It applies both inbound and outbound rules to target security groups. 6 | 7 | ### Download 8 | 9 | * [https://github.com/c1982/sgsync/releases](https://github.com/c1982/sgsync/releases) 10 | 11 | ### Installation 12 | 13 | * copy sgsync binary file to /usr/local/bin/sgsync 14 | * create config file to /etc/sgsync/config.yaml 15 | * create service file to /etc/systemd/system/sgsync.service 16 | 17 | sgsync.service: 18 | ```ini 19 | [Unit] 20 | Description=SGSYNC service 21 | After=network.target 22 | StartLimitIntervalSec=0 23 | 24 | [Service] 25 | Type=simple 26 | Restart=always 27 | RestartSec=1 28 | ExecStart=/usr/local/bin/sgsync --config=/etc/sgsync/config.yaml 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | ``` 33 | 34 | ### Configuration 35 | 36 | config.yaml: 37 | 38 | ```yaml 39 | interval: 5m 40 | 41 | source: 42 | aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID 43 | aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY 44 | region: eu-central-1 45 | group_id: sg-00000000000000001 46 | 47 | destinations: 48 | - 49 | aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID 50 | aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY 51 | region: us-west-2 52 | group_ids: ["sg-00000000000000002","sg-00000000000000003"] 53 | - 54 | aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID 55 | aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY 56 | region: ap-east-1 57 | group_ids: ["sg-00000000000000004"] 58 | ``` 59 | 60 | ### AWS Policy 61 | 62 | AWS IAM Policy: 63 | 64 | ```json 65 | { 66 | "Version": "2012-10-17", 67 | "Statement": [ 68 | { 69 | "Sid": "VisualEditor0", 70 | "Effect": "Allow", 71 | "Action": [ 72 | "ec2:RevokeSecurityGroupIngress", 73 | "ec2:AuthorizeSecurityGroupEgress", 74 | "ec2:AuthorizeSecurityGroupIngress", 75 | "ec2:UpdateSecurityGroupRuleDescriptionsEgress", 76 | "ec2:DescribeSecurityGroupReferences", 77 | "ec2:CreateSecurityGroup", 78 | "ec2:RevokeSecurityGroupEgress", 79 | "ec2:DescribeSecurityGroups", 80 | "ec2:UpdateSecurityGroupRuleDescriptionsIngress", 81 | "ec2:DescribeStaleSecurityGroups" 82 | ], 83 | "Resource": "*" 84 | } 85 | ] 86 | } 87 | ``` 88 | 89 | ### Contact 90 | 91 | Oğuzhan - [@c1982](https://twitter.com/c1982) - aspsrc@gmail.com 92 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/ec2" 10 | ) 11 | 12 | type AWSClient struct { 13 | configuration SyncConfig 14 | } 15 | 16 | func NewAWSClient(cfg SyncConfig) *AWSClient { 17 | return &AWSClient{ 18 | configuration: cfg, 19 | } 20 | } 21 | 22 | func (a *AWSClient) GetSourceSecurityGroup() (*ec2.SecurityGroup, error) { 23 | 24 | groups, err := a.describeSecurityGroups(a.configuration.Source.Region, 25 | a.configuration.Source.AWSAccessKeyID, 26 | a.configuration.Source.AWSSectedAccessKey, 27 | []string{a.configuration.Source.GroupID}) 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return groups[0], nil 34 | } 35 | 36 | func (a *AWSClient) GetDestinationSecurityGroups() ([]*ec2.SecurityGroup, error) { 37 | 38 | sglist := []*ec2.SecurityGroup{} 39 | 40 | if len(a.configuration.Destinations) < 1 { 41 | return nil, fmt.Errorf("there are no destination security groups") 42 | } 43 | 44 | for _, dst := range a.configuration.Destinations { 45 | 46 | if len(dst.GroupIDs) == 0 { 47 | continue 48 | } 49 | 50 | groups, err := a.describeSecurityGroups(dst.Region, dst.AWSAccessKeyID, dst.AWSSectedAccessKey, dst.GroupIDs) 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | sglist = append(sglist, groups...) 57 | } 58 | 59 | return sglist, nil 60 | } 61 | 62 | func (a *AWSClient) AuthorizeIngress(rules []*ec2.SecurityGroup) (err error) { 63 | 64 | if len(rules) < 1 { 65 | return 66 | } 67 | 68 | err = a.executeIngress(rules, func(groupid *string, permissions []*ec2.IpPermission, svc *ec2.EC2) error { 69 | _, err := svc.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ 70 | GroupId: groupid, 71 | IpPermissions: permissions, 72 | }) 73 | 74 | return err 75 | }) 76 | 77 | return err 78 | } 79 | 80 | func (a *AWSClient) AuthorizeEgress(rules []*ec2.SecurityGroup) (err error) { 81 | if len(rules) < 1 { 82 | return 83 | } 84 | 85 | err = a.executeEgress(rules, func(groupid *string, permissions []*ec2.IpPermission, svc *ec2.EC2) error { 86 | _, err := svc.AuthorizeSecurityGroupEgress(&ec2.AuthorizeSecurityGroupEgressInput{ 87 | GroupId: groupid, 88 | IpPermissions: permissions, 89 | }) 90 | 91 | return err 92 | }) 93 | 94 | return err 95 | } 96 | 97 | func (a *AWSClient) RevokeIngress(rules []*ec2.SecurityGroup) (err error) { 98 | 99 | if len(rules) < 1 { 100 | return 101 | } 102 | 103 | err = a.executeIngress(rules, func(groupid *string, permissions []*ec2.IpPermission, svc *ec2.EC2) error { 104 | _, err := svc.RevokeSecurityGroupIngress(&ec2.RevokeSecurityGroupIngressInput{ 105 | GroupId: groupid, 106 | IpPermissions: permissions, 107 | }) 108 | 109 | return err 110 | }) 111 | 112 | return err 113 | } 114 | 115 | func (a *AWSClient) RevokeEgress(rules []*ec2.SecurityGroup) (err error) { 116 | 117 | if len(rules) < 1 { 118 | return 119 | } 120 | 121 | err = a.executeEgress(rules, func(groupid *string, permissions []*ec2.IpPermission, svc *ec2.EC2) error { 122 | _, err := svc.RevokeSecurityGroupEgress(&ec2.RevokeSecurityGroupEgressInput{ 123 | GroupId: groupid, 124 | IpPermissions: permissions, 125 | }) 126 | 127 | return err 128 | }) 129 | 130 | return err 131 | } 132 | 133 | func (a *AWSClient) executeIngress(rules []*ec2.SecurityGroup, action func(*string, []*ec2.IpPermission, *ec2.EC2) error) (err error) { 134 | 135 | for _, destination := range a.configuration.Destinations { 136 | 137 | svc := ec2.New(session.Must(session.NewSession(&aws.Config{ 138 | Region: aws.String(destination.Region), 139 | Credentials: credentials.NewStaticCredentials(destination.AWSAccessKeyID, destination.AWSSectedAccessKey, "")}, 140 | ))) 141 | 142 | for _, rule := range rules { 143 | err := action(rule.GroupId, rule.IpPermissions, svc) 144 | if err != nil { 145 | break 146 | } 147 | } 148 | } 149 | 150 | return err 151 | } 152 | 153 | func (a *AWSClient) executeEgress(rules []*ec2.SecurityGroup, action func(*string, []*ec2.IpPermission, *ec2.EC2) error) (err error) { 154 | 155 | for _, destination := range a.configuration.Destinations { 156 | 157 | svc := ec2.New(session.Must(session.NewSession(&aws.Config{ 158 | Region: aws.String(destination.Region), 159 | Credentials: credentials.NewStaticCredentials(destination.AWSAccessKeyID, destination.AWSSectedAccessKey, "")}, 160 | ))) 161 | 162 | for _, rule := range rules { 163 | err := action(rule.GroupId, rule.IpPermissionsEgress, svc) 164 | if err != nil { 165 | break 166 | } 167 | } 168 | } 169 | 170 | return err 171 | } 172 | 173 | func (a *AWSClient) describeSecurityGroups(region, accesskey, secretkey string, groupIDs []string) ([]*ec2.SecurityGroup, error) { 174 | 175 | if len(groupIDs) < 1 { 176 | return nil, fmt.Errorf("security group id cannot be empty") 177 | } 178 | 179 | if region == "" { 180 | return nil, fmt.Errorf("region parameter cannot be empty") 181 | } 182 | 183 | if accesskey == "" { 184 | return nil, fmt.Errorf("access key cannot be empty for %s region", region) 185 | } 186 | 187 | if secretkey == "" { 188 | return nil, fmt.Errorf("secret key cannot be empty for %s region", region) 189 | } 190 | 191 | svc := ec2.New(session.Must(session.NewSession(&aws.Config{ 192 | Region: aws.String(region), 193 | Credentials: credentials.NewStaticCredentials( 194 | accesskey, 195 | secretkey, "")}, 196 | ))) 197 | 198 | input := &ec2.DescribeSecurityGroupsInput{ 199 | Filters: []*ec2.Filter{ 200 | &ec2.Filter{ 201 | Name: aws.String("group-id"), 202 | Values: aws.StringSlice(groupIDs), 203 | }, 204 | }, 205 | } 206 | 207 | output, err := svc.DescribeSecurityGroups(input) 208 | 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | if len(output.SecurityGroups) == 0 { 214 | return nil, fmt.Errorf("security group (%v) cannot find on %s region\n", 215 | groupIDs, 216 | region) 217 | } 218 | 219 | return output.SecurityGroups, nil 220 | } 221 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_GetSourceSecurityGroup(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type SyncConfig struct { 10 | Interval string `yaml:"interval"` 11 | DeleteDestinationRules bool `yaml:"delete_destination_rules"` 12 | Source SourceConfig `yaml:"source"` 13 | Destinations []DestinationConfig `yaml:"destinations"` 14 | } 15 | 16 | func NewSyncConfig(path string) (*SyncConfig, error) { 17 | cfg := &SyncConfig{} 18 | 19 | dat, err := ioutil.ReadFile(path) 20 | 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | err = yaml.Unmarshal(dat, cfg) 26 | return cfg, err 27 | } 28 | 29 | type SourceConfig struct { 30 | AWSAccessKeyID string `yaml:"aws_access_key_id"` 31 | AWSSectedAccessKey string `yaml:"aws_secret_access_key"` 32 | Region string `yaml:"region"` 33 | GroupID string `yaml:"group_id"` 34 | } 35 | 36 | type DestinationConfig struct { 37 | AWSAccessKeyID string `yaml:"aws_access_key_id"` 38 | AWSSectedAccessKey string `yaml:"aws_secret_access_key"` 39 | Region string `yaml:"region"` 40 | GroupIDs []string `yaml:"group_ids"` 41 | } 42 | -------------------------------------------------------------------------------- /config_sample.yaml: -------------------------------------------------------------------------------- 1 | interval: 5m 2 | 3 | source: 4 | aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID 5 | aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY 6 | region: eu-central-1 7 | group_id: sg-00000000000000001 8 | 9 | destinations: 10 | - 11 | aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID 12 | aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY 13 | region: us-west-2 14 | group_ids: ["sg-00000000000000002","sg-00000000000000003"] 15 | - 16 | aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID 17 | aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY 18 | region: ap-east-1 19 | group_ids: ["sg-00000000000000004"] 20 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | yaml "gopkg.in/yaml.v2" 8 | ) 9 | 10 | var testdata = `interval: 1m 11 | delete_destination_rules: true 12 | 13 | source: 14 | aws_access_key_id: ACCESSKEY 15 | aws_secret_access_key: SECRETKEY 16 | region: eu-central-1 17 | group_id: sg-1 18 | 19 | destinations: 20 | - 21 | aws_access_key_id: ACCESSKEY1 22 | aws_secret_access_key: SECRETKEY1 23 | region: us-west-1 24 | group_ids: ["sg-1","sg-2"] 25 | - 26 | aws_access_key_id: ACCESSKEY2 27 | aws_secret_access_key: SECRETKEY2 28 | region: eu-east-1 29 | group_ids: ["sg-1","sg-2","sg-3"]` 30 | 31 | func Test_ParseConfig(t *testing.T) { 32 | 33 | cfg := SyncConfig{} 34 | 35 | err := yaml.Unmarshal([]byte(testdata), &cfg) 36 | if err != nil { 37 | log.Fatalf("error: %v", err) 38 | } 39 | 40 | if cfg.Interval != "1m" { 41 | t.Errorf("interval value not expected got: %s, want: %s", cfg.Interval, "1m") 42 | } 43 | 44 | if cfg.Source.AWSAccessKeyID == "ACCESSKEY1" { 45 | t.Errorf("source accesskeyid not expected got: %s, want: %s", cfg.Source.AWSAccessKeyID, "ACCESSKEY1") 46 | } 47 | 48 | if len(cfg.Destinations) != 2 { 49 | t.Errorf("destinations array size not expected got: %d, want: %d", len(cfg.Destinations), 2) 50 | } 51 | 52 | if len(cfg.Destinations[1].GroupIDs) != 3 { 53 | t.Errorf("destinations group array size not expected got: %d, want: %d", len(cfg.Destinations[1].GroupIDs), 3) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sgsync 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.27.0 7 | github.com/rs/zerolog v1.17.2 8 | gopkg.in/yaml.v2 v2.2.7 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk= 2 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 3 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 4 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 5 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 6 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 7 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 8 | github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo= 9 | github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= 10 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 12 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 13 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 17 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 20 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | "github.com/rs/zerolog" 10 | log "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func main() { 14 | 15 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 16 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 17 | 18 | configPath := flag.String("config", "config.yaml", "--config=config.yaml") 19 | flag.Parse() 20 | 21 | if *configPath == "" { 22 | log.Error().Msg("Config parameter value cannot be empty. Please pass --config parameter value") 23 | return 24 | } 25 | 26 | cfg, err := NewSyncConfig(*configPath) 27 | if err != nil { 28 | log.Error().Str("config_file", *configPath).Err(err).Msg("configuration error") 29 | return 30 | } 31 | 32 | interval, err := time.ParseDuration(cfg.Interval) 33 | if err != nil { 34 | log.Error().Err(err).Msg("interval cannot be expected format") 35 | return 36 | } 37 | 38 | client := NewAWSClient(*cfg) 39 | 40 | log.Info().Msg("scheduling started") 41 | log.Info().Str("interval", cfg.Interval).Msg("it has been set interval") 42 | 43 | for range time.Tick(interval) { 44 | 45 | src, dsts, err := describeAWSSecurityGroups(client) 46 | if err != nil { 47 | log.Error().Err(err).Msg("cannot describe security groups from AWS") 48 | continue 49 | } 50 | 51 | err = executeSecurityGroupFunctions(src, dsts, client) 52 | if err != nil { 53 | log.Error().Err(err).Msg("operation error") 54 | } 55 | } 56 | } 57 | 58 | func describeAWSSecurityGroups(client *AWSClient) (src *ec2.SecurityGroup, dsts []*ec2.SecurityGroup, err error) { 59 | 60 | src, err = client.GetSourceSecurityGroup() 61 | if err != nil { 62 | return nil, nil, err 63 | } 64 | 65 | dsts, err = client.GetDestinationSecurityGroups() 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | return src, dsts, err 71 | } 72 | 73 | func executeSecurityGroupFunctions(src *ec2.SecurityGroup, destinations []*ec2.SecurityGroup, client *AWSClient) (err error) { 74 | 75 | sync := NewSyncGroup(src, destinations) 76 | ingress := sync.willbeAddedIngress() 77 | egress := sync.willbeAddedEgress() 78 | revokeingress := sync.willbeDeleteIngress() 79 | revokeegress := sync.willbeDeleteEgress() 80 | 81 | log.Debug().Str("ingress", fmt.Sprintf("%v", ingress)).Msg("ingress operations executing") 82 | 83 | err = client.AuthorizeIngress(ingress) 84 | if err != nil { 85 | log.Debug().Err(err).Msg("ingress operation error") 86 | return err 87 | } 88 | 89 | log.Debug().Str("egress", fmt.Sprintf("%v", egress)).Msg("egress operations executing") 90 | 91 | err = client.AuthorizeEgress(egress) 92 | if err != nil { 93 | log.Debug().Err(err).Msg("egress operation error") 94 | return err 95 | } 96 | 97 | log.Debug().Str("revokeingress", fmt.Sprintf("%v", revokeingress)).Msg("revoke ingress operations executing") 98 | 99 | err = client.RevokeIngress(revokeingress) 100 | if err != nil { 101 | log.Debug().Err(err).Msg("revoke ingress operation error") 102 | return err 103 | } 104 | 105 | log.Debug().Str("revokeegress", fmt.Sprintf("%v", revokeegress)).Msg("revoke egress operations executing") 106 | 107 | err = client.RevokeEgress(revokeegress) 108 | if err != nil { 109 | log.Debug().Err(err).Msg("revoke egress operation error") 110 | } 111 | return err 112 | } 113 | -------------------------------------------------------------------------------- /syncgroup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/ec2" 5 | ) 6 | 7 | type syncGroup struct { 8 | source *ec2.SecurityGroup 9 | destinations []*ec2.SecurityGroup 10 | } 11 | 12 | func NewSyncGroup(source *ec2.SecurityGroup, securityGroups []*ec2.SecurityGroup) *syncGroup { 13 | return &syncGroup{ 14 | source: source, 15 | destinations: securityGroups, 16 | } 17 | } 18 | 19 | func (s *syncGroup) willbeAddedIngress() []*ec2.SecurityGroup { 20 | 21 | willbeAddedIngress := []*ec2.SecurityGroup{} 22 | 23 | for _, dst := range s.destinations { 24 | 25 | newsg := s.popsg(dst) 26 | 27 | for _, src := range s.source.IpPermissions { 28 | 29 | exists, dstprm := s.isPermissionExists(src, dst.IpPermissions) 30 | 31 | if !exists { 32 | newperm := s.popperm(src) 33 | newperm.SetIpRanges(src.IpRanges) 34 | newperm.SetIpv6Ranges(src.Ipv6Ranges) 35 | newsg.IpPermissions = append(newsg.IpPermissions, newperm) 36 | } else { 37 | 38 | updatesg := s.popsg(dst) 39 | updateperm := s.popperm(dstprm) 40 | 41 | //ipv4 42 | for _, srcrange := range src.IpRanges { 43 | rangexists, _ := s.isIpRangeExists(srcrange, dstprm.IpRanges) 44 | if !rangexists { 45 | updateperm.IpRanges = append(updateperm.IpRanges, srcrange) 46 | } 47 | } 48 | 49 | //ipv6 50 | for _, srcrange6 := range src.Ipv6Ranges { 51 | 52 | rangexist, _ := s.isIpv6RangeExists(srcrange6, dstprm.Ipv6Ranges) 53 | if !rangexist { 54 | updateperm.Ipv6Ranges = append(updateperm.Ipv6Ranges, srcrange6) 55 | } 56 | } 57 | 58 | if len(updateperm.Ipv6Ranges) > 0 || len(updateperm.IpRanges) > 0 { 59 | 60 | updatesg.SetIpPermissions([]*ec2.IpPermission{ 61 | updateperm, 62 | }) 63 | 64 | willbeAddedIngress = append(willbeAddedIngress, updatesg) 65 | } 66 | } 67 | } 68 | 69 | if len(newsg.IpPermissions) > 0 { 70 | willbeAddedIngress = append(willbeAddedIngress, newsg) 71 | } 72 | } 73 | 74 | return willbeAddedIngress 75 | } 76 | 77 | func (s *syncGroup) willbeAddedEgress() []*ec2.SecurityGroup { 78 | 79 | willbeAddedEgress := []*ec2.SecurityGroup{} 80 | 81 | for _, dst := range s.destinations { 82 | 83 | newsg := s.popsg(dst) 84 | 85 | for _, src := range s.source.IpPermissionsEgress { 86 | 87 | exists, dstprm := s.isPermissionExists(src, dst.IpPermissionsEgress) 88 | 89 | if !exists { 90 | newperm := s.popperm(src) 91 | newperm.SetIpRanges(src.IpRanges) 92 | newperm.SetIpv6Ranges(src.Ipv6Ranges) 93 | newsg.IpPermissionsEgress = append(newsg.IpPermissionsEgress, newperm) 94 | } else { 95 | 96 | updatesg := s.popsg(dst) 97 | updateperm := s.popperm(dstprm) 98 | 99 | //ipv4 100 | for _, srcrange := range src.IpRanges { 101 | rangexists, _ := s.isIpRangeExists(srcrange, dstprm.IpRanges) 102 | if !rangexists { 103 | updateperm.IpRanges = append(updateperm.IpRanges, srcrange) 104 | } 105 | } 106 | 107 | //ipv6 108 | for _, srcrange6 := range src.Ipv6Ranges { 109 | rangexist, _ := s.isIpv6RangeExists(srcrange6, dstprm.Ipv6Ranges) 110 | 111 | if !rangexist { 112 | updateperm.Ipv6Ranges = append(updateperm.Ipv6Ranges, srcrange6) 113 | } 114 | } 115 | 116 | if len(updateperm.Ipv6Ranges) > 0 || len(updateperm.IpRanges) > 0 { 117 | updatesg.SetIpPermissionsEgress([]*ec2.IpPermission{ 118 | updateperm, 119 | }) 120 | 121 | willbeAddedEgress = append(willbeAddedEgress, updatesg) 122 | } 123 | } 124 | } 125 | 126 | if len(newsg.IpPermissionsEgress) > 0 { 127 | willbeAddedEgress = append(willbeAddedEgress, newsg) 128 | } 129 | } 130 | 131 | return willbeAddedEgress 132 | } 133 | 134 | func (s *syncGroup) willbeDeleteIngress() []*ec2.SecurityGroup { 135 | 136 | willbeDeleteIngress := []*ec2.SecurityGroup{} 137 | 138 | for _, dst := range s.destinations { 139 | 140 | newsg := s.popsg(dst) 141 | 142 | for _, dstpermission := range dst.IpPermissions { 143 | 144 | newperm := s.popperm(dstpermission) 145 | exists, srcpermission := s.isPermissionExists(dstpermission, s.source.IpPermissions) 146 | 147 | if !exists { 148 | newperm.SetIpRanges(dstpermission.IpRanges) 149 | newperm.SetIpv6Ranges(dstpermission.Ipv6Ranges) 150 | } else { 151 | 152 | for _, dstrange := range dstpermission.IpRanges { 153 | 154 | rangexists, _ := s.isIpRangeExists(dstrange, srcpermission.IpRanges) 155 | if !rangexists { 156 | newperm.IpRanges = append(newperm.IpRanges, dstrange) 157 | } 158 | } 159 | 160 | for _, dstrange := range dstpermission.Ipv6Ranges { 161 | 162 | rangexists, _ := s.isIpv6RangeExists(dstrange, srcpermission.Ipv6Ranges) 163 | if !rangexists { 164 | newperm.Ipv6Ranges = append(newperm.Ipv6Ranges, dstrange) 165 | } 166 | } 167 | } 168 | 169 | if len(newperm.IpRanges) > 0 || len(newperm.Ipv6Ranges) > 0 { 170 | newsg.IpPermissions = append(newsg.IpPermissions, newperm) 171 | } 172 | } 173 | 174 | if len(newsg.IpPermissions) > 0 { 175 | willbeDeleteIngress = append(willbeDeleteIngress, newsg) 176 | } 177 | } 178 | 179 | return willbeDeleteIngress 180 | } 181 | 182 | func (s *syncGroup) willbeDeleteEgress() []*ec2.SecurityGroup { 183 | 184 | willbeDeleteEgress := []*ec2.SecurityGroup{} 185 | 186 | for _, dst := range s.destinations { 187 | 188 | newsg := s.popsg(dst) 189 | 190 | for _, dstpermission := range dst.IpPermissionsEgress { 191 | 192 | newperm := s.popperm(dstpermission) 193 | exists, srcpermission := s.isPermissionExists(dstpermission, s.source.IpPermissionsEgress) 194 | 195 | if !exists { 196 | newperm.SetIpRanges(dstpermission.IpRanges) 197 | newperm.SetIpv6Ranges(dstpermission.Ipv6Ranges) 198 | } else { 199 | 200 | for _, dstrange := range dstpermission.IpRanges { 201 | 202 | rangexists, _ := s.isIpRangeExists(dstrange, srcpermission.IpRanges) 203 | if !rangexists { 204 | newperm.IpRanges = append(newperm.IpRanges, dstrange) 205 | } 206 | } 207 | 208 | for _, dstrange := range dstpermission.Ipv6Ranges { 209 | 210 | rangexists, _ := s.isIpv6RangeExists(dstrange, srcpermission.Ipv6Ranges) 211 | if !rangexists { 212 | newperm.Ipv6Ranges = append(newperm.Ipv6Ranges, dstrange) 213 | } 214 | } 215 | } 216 | 217 | if len(newperm.IpRanges) > 0 || len(newperm.Ipv6Ranges) > 0 { 218 | newsg.IpPermissionsEgress = append(newsg.IpPermissionsEgress, newperm) 219 | } 220 | } 221 | 222 | if len(newsg.IpPermissionsEgress) > 0 { 223 | willbeDeleteEgress = append(willbeDeleteEgress, newsg) 224 | } 225 | } 226 | 227 | return willbeDeleteEgress 228 | } 229 | 230 | func (s *syncGroup) isPermissionExists(src *ec2.IpPermission, destinations []*ec2.IpPermission) (bool, *ec2.IpPermission) { 231 | 232 | for _, dst := range destinations { 233 | if dst.FromPort != nil && dst.ToPort != nil && src.FromPort != nil && src.ToPort != nil { 234 | if *src.FromPort == *dst.FromPort && *src.ToPort == *dst.ToPort && *src.IpProtocol == *dst.IpProtocol { 235 | return true, dst 236 | } 237 | } 238 | 239 | if dst.FromPort == nil && dst.ToPort == nil && src.FromPort == nil && src.ToPort == nil { 240 | if *src.IpProtocol == *dst.IpProtocol { 241 | return true, dst 242 | } 243 | } 244 | } 245 | 246 | return false, nil 247 | } 248 | 249 | func (s *syncGroup) isIpRangeExists(src *ec2.IpRange, destinations []*ec2.IpRange) (bool, *ec2.IpRange) { 250 | 251 | for _, dst := range destinations { 252 | if *dst.CidrIp == *src.CidrIp { 253 | return true, dst 254 | } 255 | } 256 | 257 | return false, nil 258 | } 259 | 260 | func (s *syncGroup) isIpv6RangeExists(src *ec2.Ipv6Range, destinations []*ec2.Ipv6Range) (bool, *ec2.Ipv6Range) { 261 | 262 | for _, dst := range destinations { 263 | if *dst.CidrIpv6 == *src.CidrIpv6 { 264 | return true, dst 265 | } 266 | } 267 | 268 | return false, nil 269 | } 270 | 271 | func (s *syncGroup) popsg(sg *ec2.SecurityGroup) *ec2.SecurityGroup { 272 | 273 | newsg := &ec2.SecurityGroup{} 274 | 275 | if sg.GroupId != nil { 276 | newsg.SetGroupId(*sg.GroupId) 277 | } 278 | 279 | if sg.GroupName != nil { 280 | newsg.SetGroupName(*sg.GroupName) 281 | } 282 | 283 | if sg.Description != nil { 284 | newsg.SetDescription(*sg.Description) 285 | } 286 | 287 | if sg.OwnerId != nil { 288 | newsg.SetOwnerId(*sg.OwnerId) 289 | } 290 | 291 | if sg.VpcId != nil { 292 | newsg.SetVpcId(*sg.VpcId) 293 | } 294 | 295 | if sg.Tags != nil { 296 | newsg.SetTags(sg.Tags) 297 | } 298 | 299 | return newsg 300 | } 301 | 302 | func (s *syncGroup) popperm(perm *ec2.IpPermission) *ec2.IpPermission { 303 | 304 | newperm := &ec2.IpPermission{} 305 | 306 | if perm.FromPort != nil { 307 | newperm.SetFromPort(*perm.FromPort) 308 | } 309 | 310 | if perm.ToPort != nil { 311 | newperm.SetToPort(*perm.ToPort) 312 | } 313 | 314 | if perm.IpProtocol != nil { 315 | newperm.SetIpProtocol(*perm.IpProtocol) 316 | } 317 | 318 | return newperm 319 | } 320 | -------------------------------------------------------------------------------- /syncgroup_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | 8 | "github.com/aws/aws-sdk-go/service/ec2" 9 | ) 10 | 11 | var sourceGroup = []*ec2.SecurityGroup{ 12 | (&ec2.SecurityGroup{ 13 | Description: aws.String("source"), 14 | GroupId: aws.String("gs-001"), 15 | GroupName: aws.String("source group"), 16 | OwnerId: aws.String("owner"), 17 | VpcId: aws.String("wpc"), 18 | IpPermissions: []*ec2.IpPermission{ 19 | (&ec2.IpPermission{ 20 | FromPort: aws.Int64(22), 21 | ToPort: aws.Int64(22), 22 | IpProtocol: aws.String("tcp"), 23 | IpRanges: []*ec2.IpRange{ 24 | (&ec2.IpRange{ 25 | CidrIp: aws.String("10.5.5.1/32"), 26 | }), 27 | }, 28 | }), 29 | }, 30 | IpPermissionsEgress: []*ec2.IpPermission{ 31 | (&ec2.IpPermission{ 32 | IpProtocol: aws.String("-1"), 33 | IpRanges: []*ec2.IpRange{ 34 | (&ec2.IpRange{ 35 | CidrIp: aws.String("0.0.0.0/0"), 36 | }), 37 | }, 38 | }), 39 | (&ec2.IpPermission{ 40 | IpProtocol: aws.String("-1"), 41 | IpRanges: []*ec2.IpRange{ 42 | (&ec2.IpRange{ 43 | CidrIp: aws.String("5.5.5.2/32"), 44 | }), 45 | }, 46 | }), 47 | }, 48 | }), 49 | } 50 | 51 | var destinations = []*ec2.SecurityGroup{ 52 | (&ec2.SecurityGroup{ 53 | Description: aws.String("destination 2"), 54 | GroupId: aws.String("gs-002"), 55 | GroupName: aws.String("destination group 2"), 56 | OwnerId: aws.String("owner-id"), 57 | VpcId: aws.String("vpc-id"), 58 | IpPermissions: []*ec2.IpPermission{ 59 | (&ec2.IpPermission{ 60 | FromPort: aws.Int64(22), 61 | ToPort: aws.Int64(22), 62 | IpProtocol: aws.String("tcp"), 63 | IpRanges: []*ec2.IpRange{ 64 | (&ec2.IpRange{ 65 | CidrIp: aws.String("10.5.5.1/32"), 66 | }), 67 | }, 68 | }), 69 | (&ec2.IpPermission{ 70 | FromPort: aws.Int64(22), 71 | ToPort: aws.Int64(22), 72 | IpProtocol: aws.String("tcp"), 73 | IpRanges: []*ec2.IpRange{ 74 | (&ec2.IpRange{ 75 | CidrIp: aws.String("4.2.2.1/32"), 76 | }), 77 | }, 78 | }), 79 | }, 80 | IpPermissionsEgress: []*ec2.IpPermission{ 81 | (&ec2.IpPermission{ 82 | FromPort: aws.Int64(53), 83 | ToPort: aws.Int64(53), 84 | IpProtocol: aws.String("udp"), 85 | IpRanges: []*ec2.IpRange{ 86 | (&ec2.IpRange{ 87 | CidrIp: aws.String("8.8.8.8/32"), 88 | }), 89 | }, 90 | }), 91 | }, 92 | }), 93 | (&ec2.SecurityGroup{ 94 | Description: aws.String("destination 3"), 95 | GroupId: aws.String("gs-003"), 96 | GroupName: aws.String("destination group 3"), 97 | OwnerId: aws.String("owner-id"), 98 | VpcId: aws.String("vpc-id"), 99 | IpPermissions: []*ec2.IpPermission{ 100 | (&ec2.IpPermission{ 101 | FromPort: aws.Int64(22), 102 | ToPort: aws.Int64(22), 103 | IpProtocol: aws.String("tcp"), 104 | IpRanges: []*ec2.IpRange{ 105 | (&ec2.IpRange{ 106 | CidrIp: aws.String("8.8.8.8/32"), 107 | }), 108 | }, 109 | }), 110 | }, 111 | }), 112 | (&ec2.SecurityGroup{ 113 | Description: aws.String("destination 4"), 114 | GroupId: aws.String("gs-004"), 115 | GroupName: aws.String("destination group 4"), 116 | OwnerId: aws.String("owner-id"), 117 | VpcId: aws.String("vpc-id"), 118 | IpPermissions: []*ec2.IpPermission{ 119 | (&ec2.IpPermission{ 120 | FromPort: aws.Int64(8080), 121 | ToPort: aws.Int64(8080), 122 | IpProtocol: aws.String("tcp"), 123 | IpRanges: []*ec2.IpRange{ 124 | (&ec2.IpRange{ 125 | CidrIp: aws.String("0.0.0.0/0"), 126 | }), 127 | }, 128 | }), 129 | }, 130 | IpPermissionsEgress: []*ec2.IpPermission{ 131 | (&ec2.IpPermission{ 132 | IpProtocol: aws.String("-1"), 133 | IpRanges: []*ec2.IpRange{ 134 | (&ec2.IpRange{ 135 | CidrIp: aws.String("0.0.0.0/0"), 136 | }), 137 | }, 138 | }), 139 | (&ec2.IpPermission{ 140 | FromPort: aws.Int64(443), 141 | ToPort: aws.Int64(443), 142 | IpProtocol: aws.String("tcp"), 143 | IpRanges: []*ec2.IpRange{ 144 | (&ec2.IpRange{ 145 | CidrIp: aws.String("0.0.0.0/0"), 146 | }), 147 | }, 148 | }), 149 | }, 150 | }), 151 | } 152 | 153 | var dstengress = []*ec2.SecurityGroup{ 154 | (&ec2.SecurityGroup{ 155 | Description: aws.String("destination 4"), 156 | GroupId: aws.String("gs-004"), 157 | GroupName: aws.String("destination group 4"), 158 | OwnerId: aws.String("owner-id"), 159 | VpcId: aws.String("vpc-id"), 160 | IpPermissionsEgress: []*ec2.IpPermission{ 161 | (&ec2.IpPermission{ 162 | IpProtocol: aws.String("-1"), 163 | IpRanges: []*ec2.IpRange{ 164 | (&ec2.IpRange{ 165 | CidrIp: aws.String("0.0.0.0/0"), 166 | }), 167 | }, 168 | }), 169 | }, 170 | }), 171 | } 172 | 173 | func Test_WillbeAddedIngress(t *testing.T) { 174 | 175 | s := NewSyncGroup(sourceGroup[0], destinations) 176 | ingress := s.willbeAddedIngress() 177 | 178 | if len(ingress) != 2 { 179 | t.Errorf("ingress rules cannot prepare. got: %d, want: %d", len(ingress), 2) 180 | } 181 | } 182 | 183 | func Test_WillbeAddedEgress(t *testing.T) { 184 | 185 | s := NewSyncGroup(sourceGroup[0], dstengress) 186 | egress := s.willbeAddedEgress() 187 | 188 | if len(egress) != 1 { 189 | t.Errorf("egress value not expected got: %d, want: %d", len(egress), 1) 190 | } 191 | } 192 | 193 | func Test_WillBeDeleteIngress(t *testing.T) { 194 | 195 | s := NewSyncGroup(sourceGroup[0], destinations) 196 | deleteds := s.willbeDeleteIngress() 197 | 198 | if len(deleteds) != 3 { 199 | t.Errorf("deleted ingress value not expected got: %d, want: %d", len(deleteds), 3) 200 | } 201 | } 202 | 203 | func Test_WillBeDeleteEgress(t *testing.T) { 204 | s := NewSyncGroup(sourceGroup[0], destinations) 205 | deleteds := s.willbeDeleteEgress() 206 | 207 | if len(deleteds) != 2 { 208 | t.Errorf("deleted egress value not expected got: %d, want: %d", len(deleteds), 2) 209 | } 210 | } 211 | --------------------------------------------------------------------------------