Skip to content

Commit

Permalink
Merge pull request #1 from DoctorOgg/v2
Browse files Browse the repository at this point in the history
moving to using SQS to trigger route updates
  • Loading branch information
DoctorOgg authored Feb 6, 2024
2 parents b311b4d + eca7baf commit 29a501f
Show file tree
Hide file tree
Showing 12 changed files with 491 additions and 126 deletions.
72 changes: 69 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,79 @@
# tailscale-route-tiler

This helper tool generates a list of subnets for TailScale and runs the TailScale command to update the routes.
The `tailscale-route-tiler` is a helper tool designed to automate the management of network routes for Tailscale, using configuration changes detected through AWS CloudWatch and SQS. This utility listens for messages on a specified AWS SQS queue, triggered by CloudWatch events, indicating when route updates are needed. It requires a YAML configuration file for initial setup SQS queue details, and Tailscale authentication information.

This tool uses a YAML configuration file; you can find an example in the repo. The Client ID and Tailscale key are required.
## AWS Setup

To use `tailscale-route-tiler`, you must configure AWS CloudWatch and SQS services to trigger route updates:

1. **SQS Queue Creation**: Create an SQS queue in AWS to receive CloudWatch events. Note the queue URL and ARN for configuration.

2. **CloudWatch Rule Configuration**: Set up a CloudWatch rule to monitor specific events

```json
{
"source": ["aws.ec2"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"sourceIPAddress": ["elasticloadbalancing.amazonaws.com"],
"eventName": ["CreateNetworkInterface", "DeleteNetworkInterface"],
"requestParameters": {
"description": [{
"prefix": "ELB app/xxxxxxxx/xxxxxxx"
}, {
"prefix": "ELB app/xxxxxxx/xxxxxx"
}]
}
}
}
```

3. **IAM Permissions**: Ensure the IAM role or user running `tailscale-route-tiler` has permissions to read from the SQS queue and execute necessary Tailscale API calls.

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes"
],
"Resource": "arn:aws:sqs:your-region:your-account-id:your-queue-name"
}
]
}
```

### Configuration

The tool requires a YAML configuration file with the following structure:

```yaml
TailscaleclientId: "EXAMPLE-CLIENT-ID"
TailscaleKey: "DUMMY-KEY"
subnets:
- 10.49.0.0/24
- 10.0.0.22/32
- 10.0.0.0/24
sites:
- google.com
TailscaleCommand: /usr/bin/tailscale up --accept-dns=false --advertise-routes=%s
EnableIpv6: false
Slack:
WebhookURL: https://hooks.slack.com/services/EXAMPLE/EXAMPLE/EXAMPLE
Enabled: true
SQS:
QueueURL: https://sqs.us-west-2.amazonaws.com/EXAMPLE/EXAMPLE
Region: us-west-2
```
## Usage
```bash
tailscale-route-tiler run -c config.yaml
tailscale-route-tiler worker -c config.yaml
```

## Help
Expand Down
128 changes: 128 additions & 0 deletions cloudwatchevent/cloudwatchevent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cloudwatchevent

import "time"

// Represents the top-level structure of a CloudTrail event.
type CloudTrailEvent struct {
Version string `json:"version"`
ID string `json:"id"`
DetailType string `json:"detail-type"`
Source string `json:"source"`
Account string `json:"account"`
Time time.Time `json:"time"`
Region string `json:"region"`
Detail Detail `json:"detail"`
}

// Detail holds information about the API call event.
type Detail struct {
EventVersion string `json:"eventVersion"`
UserIdentity UserIdentity `json:"userIdentity"`
EventTime time.Time `json:"eventTime"`
EventSource string `json:"eventSource"`
EventName string `json:"eventName"`
AwsRegion string `json:"awsRegion"`
SourceIPAddress string `json:"sourceIPAddress"`
UserAgent string `json:"userAgent"`
RequestParameters RequestParameters `json:"requestParameters"`
ResponseElements ResponseElements `json:"responseElements"`
RequestID string `json:"requestID"`
EventID string `json:"eventID"`
ReadOnly bool `json:"readOnly"`
EventType string `json:"eventType"`
ManagementEvent bool `json:"managementEvent"`
RecipientAccountId string `json:"recipientAccountId"`
EventCategory string `json:"eventCategory"`
}

// UserIdentity describes the identity of the requester.
type UserIdentity struct {
Type string `json:"type"`
PrincipalID string `json:"principalId"`
ARN string `json:"arn"`
AccountId string `json:"accountId"`
SessionContext SessionContext `json:"sessionContext"`
InvokedBy string `json:"invokedBy"`
}

// SessionContext provides context for the session in which the request was made.
type SessionContext struct {
SessionIssuer SessionIssuer `json:"sessionIssuer"`
Attributes Attributes `json:"attributes"`
}

// SessionIssuer details about the entity that provided the session.
type SessionIssuer struct {
Type string `json:"type"`
PrincipalID string `json:"principalId"`
ARN string `json:"arn"`
AccountId string `json:"accountId"`
UserName string `json:"userName"`
}

// Attributes holds session attributes such as creation time and MFA authentication status.
type Attributes struct {
CreationDate time.Time `json:"creationDate"`
MfaAuthenticated string `json:"mfaAuthenticated"`
}

// RequestParameters includes parameters specific to the API call.
type RequestParameters struct {
SubnetId string `json:"subnetId"`
Description string `json:"description"`
GroupSet GroupSet `json:"groupSet"`
PrivateIpAddressesSet interface{} `json:"privateIpAddressesSet"` // Can be empty, adjust based on actual use
Ipv6AddressCount int `json:"ipv6AddressCount"`
ClientToken string `json:"clientToken"`
}

// ResponseElements contains the response from the API call.
type ResponseElements struct {
RequestId string `json:"requestId"`
NetworkInterface NetworkInterface `json:"networkInterface"`
}

// NetworkInterface details about the created or modified network interface.
type NetworkInterface struct {
NetworkInterfaceId string `json:"networkInterfaceId"`
SubnetId string `json:"subnetId"`
VpcId string `json:"vpcId"`
AvailabilityZone string `json:"availabilityZone"`
Description string `json:"description"`
OwnerId string `json:"ownerId"`
RequesterId string `json:"requesterId"`
RequesterManaged bool `json:"requesterManaged"`
Status string `json:"status"`
MacAddress string `json:"macAddress"`
PrivateIpAddress string `json:"privateIpAddress"`
PrivateDnsName string `json:"privateDnsName"`
SourceDestCheck bool `json:"sourceDestCheck"`
InterfaceType string `json:"interfaceType"`
GroupSet GroupSet `json:"groupSet"`
PrivateIpAddressesSet PrivateIpAddressesSet `json:"privateIpAddressesSet"`
Ipv6AddressesSet interface{} `json:"ipv6AddressesSet"`
TagSet interface{} `json:"tagSet"`
}

// GroupSet represents a set of security groups.
type GroupSet struct {
Items []GroupItem `json:"items"`
}

// GroupItem details of a single security group.
type GroupItem struct {
GroupId string `json:"groupId"`
GroupName string `json:"groupName"` // Not present in your JSON, included for completeness
}

// PrivateIpAddressesSet structure to match the JSON structure for private IP addresses.
type PrivateIpAddressesSet struct {
Item []PrivateIpAddressSetItem `json:"item"`
}

// PrivateIpAddressSetItem contains details about a single private IP address.
type PrivateIpAddressSetItem struct {
PrivateIpAddress string `json:"privateIpAddress"`
PrivateDnsName string `json:"privateDnsName"`
Primary bool `json:"primary"`
}
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ type Config struct {
TailscaleclientId string `yaml:"TailscaleclientId"`
TailscaleKey string `yaml:"TailscaleKey"`
Slack Slack `yaml:"Slack"`
SQS SQS `yaml:"SQS"`
}

type Slack struct {
WebhookURL string `yaml:"WebhookURL"`
Enabled bool `yaml:"Enabled"`
}

type SQS struct {
QueueURL string `yaml:"QueueURL"`
Region string `yaml:"Region"`
}

var ActiveConfig *Config

// ReadYAML reads the YAML configuration file
Expand Down
27 changes: 27 additions & 0 deletions examples/cloudwatch-rule.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"source": [
"aws.ec2"
],
"detail-type": [
"AWS API Call via CloudTrail"
],
"detail": {
"sourceIPAddress": [
"elasticloadbalancing.amazonaws.com"
],
"eventName": [
"CreateNetworkInterface",
"DeleteNetworkInterface"
],
"requestParameters": {
"description": [
{
"prefix": "ELB app/xxxxxxxx/xxxxxxx"
},
{
"prefix": "ELB app/xxxxxxx/xxxxxx"
}
]
}
}
}
3 changes: 3 additions & 0 deletions config.yml-example → examples/config.yml-example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ EnableIpv6: false
Slack:
WebhookURL: https://hooks.slack.com/services/EXAMPLE/EXAMPLE/EXAMPLE
Enabled: true
SQS:
QueueURL: https://sqs.us-west-2.amazonaws.com/EXAMPLE/EXAMPLE
Region: us-west-2
105 changes: 105 additions & 0 deletions examples/example-sqs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"version": "0",
"id": "xxxxxxxxx-dd17-e765-4df0-xxxxxxxxxx",
"detail-type": "AWS API Call via CloudTrail",
"source": "aws.ec2",
"account": "xxxxxxxxxxx",
"time": "2024-02-02T06:34:30Z",
"region": "us-west-2",
"resources": [],
"detail": {
"eventVersion": "1.09",
"userIdentity": {
"type": "AssumedRole",
"principalId": "xxxxxxxx:ElasticLoadBalancing",
"arn": "arn:aws:sts::xxxxxxx:assumed-role/AWSServiceRoleForElasticLoadBalancing/ElasticLoadBalancing",
"accountId": "xxxxxxx",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "xxxxxxxxxx",
"arn": "arn:aws:iam::xxxxxxx:role/aws-service-role/elasticloadbalancing.amazonaws.com/AWSServiceRoleForElasticLoadBalancing",
"accountId": "xxxxxxxxx",
"userName": "AWSServiceRoleForElasticLoadBalancing"
},
"attributes": {
"creationDate": "2024-02-02T06:34:30Z",
"mfaAuthenticated": "false"
}
},
"invokedBy": "elasticloadbalancing.amazonaws.com"
},
"eventTime": "2024-02-02T06:34:30Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "CreateNetworkInterface",
"awsRegion": "us-west-2",
"sourceIPAddress": "elasticloadbalancing.amazonaws.com",
"userAgent": "elasticloadbalancing.amazonaws.com",
"requestParameters": {
"subnetId": "subnet-xxxxxxx",
"description": "ELB app/xxxxxx/xxxxxxxx",
"groupSet": {
"items": [
{
"groupId": "sg-xxxxxxxx"
},
{
"groupId": "sg-xxxxxxx"
}
]
},
"privateIpAddressesSet": {},
"ipv6AddressCount": 0,
"clientToken": "xxxxx-b2xxxxb5-xxx-ae66-xxxxx"
},
"responseElements": {
"requestId": "xxxxxxx-ca5a-4526-96ca-xxxxxxxx",
"networkInterface": {
"networkInterfaceId": "eni-xxxxxxxxxx",
"subnetId": "subnet-xxxxxxx",
"vpcId": "vpc-xxxxxxxx",
"availabilityZone": "us-west-2b",
"description": "ELB app/xxxxxxx/cxxxxxxxxxxxx2",
"ownerId": "xxxxxxxxxx",
"requesterId": "amazon-elb",
"requesterManaged": true,
"status": "pending",
"macAddress": "xx:xx:xx:5a:78:77",
"privateIpAddress": "10.49.61.187",
"privateDnsName": "ixxxxxxxxx.us-west-2.compute.internal",
"sourceDestCheck": true,
"interfaceType": "interface",
"groupSet": {
"items": [
{
"groupId": "sg-xxxxxxxx",
"groupName": "default"
},
{
"groupId": "sg-xxxxxxx",
"groupName": "xxxxxx-alb"
}
]
},
"privateIpAddressesSet": {
"item": [
{
"privateIpAddress": "10.49.61.0",
"privateDnsName": "xxxxxxxxxxx.us-west-2.compute.internal",
"primary": true
}
]
},
"ipv6AddressesSet": {},
"tagSet": {}
}
},
"requestID": "xxxxxxx-ca5a-4526-96ca-xxxxxxxxx",
"eventID": "xxxxxxxx-0270-4b29-8130-xxxxxxxx",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "xxxxxxxxx",
"eventCategory": "Management"
}
}
Loading

0 comments on commit 29a501f

Please sign in to comment.