sshfp-generator: initial commit.
Fully working version. TODO: Few minor changes Signed-off-by: Artur Marud <artur@status.im>
This commit is contained in:
parent
cc366c072c
commit
be748b44d6
|
@ -7,9 +7,10 @@
|
|||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
test*
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
vendor/
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# Description
|
||||
|
||||
SSHFP Tool is a tool created in Golang to glue Consul DB and Cloudflare.
|
||||
|
||||
Main purpose is creating SSHFP records to get rid of "host key verification failed".
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
go mod -vendor
|
||||
go build -mod vendor
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Supported env variables:
|
||||
`DOMAIN_NAME` - Domain name which will be working on
|
||||
`CF_TOKEN` - CloudFlare Token with write access to above domain
|
||||
`CONSUL_TOKEN` - Consul Token with priviledges to read services
|
||||
|
||||
It's possible to create json formatted config file (example in `testcfg`)
|
||||
|
||||
As it has been designed to work with `consul watches` passing proper .json file
|
||||
to STDIN is required. Ex:
|
||||
`cat watches.dump | ./infra-sshfp-cf`
|
||||
|
||||
## Current state
|
||||
- CloudFlare integration is fully implemented
|
||||
- SSHFP Record creation based on tag in Consul form.
|
||||
- Implemented Consul watches integration
|
||||
- Implemented logic to manipulate states (merging config, etc)
|
||||
|
||||
## TODO:
|
||||
- A few major changes
|
|
@ -0,0 +1,23 @@
|
|||
package cloudflare
|
||||
|
||||
import (
|
||||
"infra-sshfp-cf/sshfp"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
FindRecords(hostname string, recordType string) ([]cloudflare.DNSRecord, error)
|
||||
CreateDNSRecord(hostname string, recordType string, payload cloudflare.DNSRecord) (int, error)
|
||||
DeleteDNSRecord(recordID string) error
|
||||
UpdateDNSRecord(hostname string, recordID string, payload cloudflare.DNSRecord) error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
FindHostByName(hostname string) (bool, error)
|
||||
GetSSHFPRecordsForHost(hostname string) ([]*sshfp.SSHFPRecord, error)
|
||||
CreateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) (int, error)
|
||||
DeleteSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error
|
||||
UpdateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error
|
||||
ApplyConfigPlan(configPlan sshfp.ConfigPlan) (int, error)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package cloudflare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type repository struct {
|
||||
api *cloudflare.API
|
||||
domainId string
|
||||
domainName string
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewRepository(token string, domain string) Repository {
|
||||
logrus.Infof("cloudflare: Creating Repository")
|
||||
api, err := cloudflare.NewWithAPIToken(token)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
id, err := api.ZoneIDByName(domain)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
return nil
|
||||
}
|
||||
return &repository{api: api, domainId: id, domainName: domain, ctx: context.Background()}
|
||||
}
|
||||
|
||||
func (r *repository) FindRecords(hostname string, recordType string) ([]cloudflare.DNSRecord, error) {
|
||||
logrus.Debugf("cloudflare: FindRecords: %s", hostname)
|
||||
|
||||
filterSet := cloudflare.DNSRecord{Type: recordType, Name: hostname + "." + r.domainName}
|
||||
|
||||
logrus.Debugf("%+v", filterSet)
|
||||
return r.api.DNSRecords(r.ctx, r.domainId, filterSet)
|
||||
}
|
||||
|
||||
func (r *repository) CreateDNSRecord(hostname string, recordType string, payload cloudflare.DNSRecord) (int, error) {
|
||||
logrus.Infof("cloudflare: CreateDNSRecord: %s", hostname)
|
||||
|
||||
payload.Name = hostname + "." + r.domainName
|
||||
|
||||
logrus.Debugf("cloudflare: CreateDNSRecord - payload: %s", payload)
|
||||
|
||||
resp, err := r.api.CreateDNSRecord(r.ctx, r.domainId, payload)
|
||||
|
||||
logrus.Debugf("%+v", resp)
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if !resp.Success {
|
||||
return -1, errors.New("cannot create record")
|
||||
}
|
||||
return resp.Count, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteDNSRecord(recordID string) error {
|
||||
logrus.Debugf("cloudflare: DeleteDNSRecord - %s", recordID)
|
||||
|
||||
return r.api.DeleteDNSRecord(r.ctx, r.domainId, recordID)
|
||||
}
|
||||
|
||||
func (r *repository) UpdateDNSRecord(hostname string, recordID string, payload cloudflare.DNSRecord) error {
|
||||
logrus.Debugf("cloudflare: UpdateeDNSRecord - %s", recordID)
|
||||
return r.api.UpdateDNSRecord(r.ctx, r.domainId, recordID, payload)
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package cloudflare
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"infra-sshfp-cf/sshfp"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
r Repository
|
||||
}
|
||||
|
||||
func NewService(r Repository) Service {
|
||||
logrus.Debug("cloudflare: Creating Service")
|
||||
return &service{r: r}
|
||||
}
|
||||
|
||||
func (s *service) FindHostByName(hostname string) (bool, error) {
|
||||
logrus.Debugf("cloudflare: FindHostByName %s", hostname)
|
||||
recs, err := s.r.FindRecords(hostname, "A")
|
||||
if err != nil {
|
||||
logrus.Errorf("CF: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(recs) > 0 {
|
||||
logrus.Infof("FindHostByName: Host found: %s", hostname)
|
||||
logrus.Debugf("%+v", recs)
|
||||
return true, nil
|
||||
}
|
||||
logrus.Infof("FindHostByName: Host not found: %s", hostname)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *service) GetSSHFPRecordsForHost(hostname string) ([]*sshfp.SSHFPRecord, error) {
|
||||
logrus.Debugf("cloudflare: GetSSHFPRecordsForHost %s", hostname)
|
||||
recs, err := s.r.FindRecords(hostname, "SSHFP")
|
||||
if err != nil {
|
||||
logrus.Errorf("CF: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
logrus.Debugf("%+v", recs)
|
||||
|
||||
output := make([]*sshfp.SSHFPRecord, 0)
|
||||
|
||||
for _, v := range recs {
|
||||
data, ok := v.Data.(map[string]interface{})
|
||||
/*
|
||||
data["type"] - float64
|
||||
data["fingerprint"] - string
|
||||
data["algorithm"] - float64
|
||||
*/
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("cannot parse data")
|
||||
}
|
||||
output = append(output, &sshfp.SSHFPRecord{RecordID: v.ID, Algorithm: fmt.Sprintf("%.f", data["algorithm"]), Type: fmt.Sprintf("%.f", data["type"]), Fingerprint: data["fingerprint"].(string)})
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *service) CreateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) (int, error) {
|
||||
logrus.Infof("cloudflare: CreateSSHFPRecord: %+v", record)
|
||||
|
||||
payload := s.preparePayloadFromSSHRecord(record)
|
||||
|
||||
return s.r.CreateDNSRecord(hostname, "SSHFP", payload)
|
||||
}
|
||||
|
||||
func (s *service) DeleteSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error {
|
||||
logrus.Infof("cloudflare: DeleteDNSRecord: %+v", record)
|
||||
|
||||
//In case we have record ID we can just delete the record.
|
||||
if record.RecordID != "" {
|
||||
return s.r.DeleteDNSRecord(record.RecordID)
|
||||
}
|
||||
|
||||
recs, err := s.GetSSHFPRecordsForHost(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//Comparing fingerprints should be more than sufficient
|
||||
for _, rec := range recs {
|
||||
if rec.Fingerprint == record.Fingerprint {
|
||||
return s.r.DeleteDNSRecord(rec.RecordID)
|
||||
}
|
||||
}
|
||||
return errors.New("record not found")
|
||||
|
||||
}
|
||||
|
||||
func (s *service) ApplyConfigPlan(configPlan sshfp.ConfigPlan) (int, error) {
|
||||
var item int = 0
|
||||
for _, v := range configPlan {
|
||||
|
||||
switch v.Operation {
|
||||
case sshfp.CREATE:
|
||||
_, err := s.CreateSSHFPRecord(v.Hostname, *v.Record)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
case sshfp.DELETE:
|
||||
err := s.DeleteSSHFPRecord(v.Hostname, *v.Record)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
case sshfp.UPDATE:
|
||||
err := s.UpdateSSHFPRecord(v.Hostname, *v.Record)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
}
|
||||
item++
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *service) UpdateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error {
|
||||
logrus.Infof("UpdateSSHFPRecord: %+v", record)
|
||||
payload := s.preparePayloadFromSSHRecord(record)
|
||||
return s.r.UpdateDNSRecord(hostname, record.RecordID, payload)
|
||||
}
|
||||
|
||||
func (s *service) preparePayloadFromSSHRecord(record sshfp.SSHFPRecord) cloudflare.DNSRecord {
|
||||
data := make(map[string]string)
|
||||
data["algorithm"] = record.Algorithm
|
||||
data["type"] = record.Type
|
||||
data["fingerprint"] = record.Fingerprint
|
||||
|
||||
return cloudflare.DNSRecord{Type: "SSHFP", Data: data, Content: fmt.Sprintf("%s %s %s", record.Algorithm, record.Type, record.Fingerprint)}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
ConsulToken string `json:"consulKey"`
|
||||
CloudflareToken string `json:"cloudflareKey"`
|
||||
DomainName string `json:"domain"`
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package config
|
||||
|
||||
type Repository interface {
|
||||
LoadFile(fileName string) error
|
||||
SaveFile(fileName string) error
|
||||
GetConfig() *Config
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
LoadConfig(fileName string) (*Config, error)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type repository struct {
|
||||
data Config
|
||||
}
|
||||
|
||||
func NewFileRepository() Repository {
|
||||
return &repository{data: Config{}}
|
||||
}
|
||||
|
||||
func (r *repository) LoadFile(fileName string) error {
|
||||
logrus.Infof("config: LoadFile %s", fileName)
|
||||
content, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(content, &r.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *repository) SaveFile(fileName string) error {
|
||||
logrus.Infof("config: SaveFile %s", fileName)
|
||||
content, err := json.MarshalIndent(r.data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(fileName, content, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) GetConfig() *Config {
|
||||
return &r.data
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
r Repository
|
||||
}
|
||||
|
||||
func NewService(r Repository) Service {
|
||||
return &service{r: r}
|
||||
}
|
||||
|
||||
func (s *service) LoadConfig(fileName string) (*Config, error) {
|
||||
if s.r == nil || fileName == "" {
|
||||
logrus.Infoln("config: LoadConfig")
|
||||
cfToken, exists := os.LookupEnv("CF_TOKEN")
|
||||
if !exists {
|
||||
return nil, errors.New("cannot find env variable CF_TOKEN")
|
||||
}
|
||||
consulToken, exists := os.LookupEnv("CONSUL_TOKEN")
|
||||
if !exists {
|
||||
return nil, errors.New("cannot find env variable CONSUL_TOKEN")
|
||||
}
|
||||
|
||||
domainaName, exists := os.LookupEnv("DOMAIN_NAME")
|
||||
if exists {
|
||||
return nil, errors.New("cannot find env variable DOMAIN_NAME")
|
||||
}
|
||||
|
||||
return &Config{ConsulToken: consulToken, CloudflareToken: cfToken, DomainName: domainaName}, nil
|
||||
|
||||
}
|
||||
|
||||
err := s.r.LoadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.r.GetConfig(), nil
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package consul
|
||||
|
||||
type host struct {
|
||||
Node struct {
|
||||
Node string
|
||||
}
|
||||
Service struct {
|
||||
Meta map[string]string
|
||||
CreateIndex int
|
||||
ModifyIndex int
|
||||
}
|
||||
}
|
||||
|
||||
type rawHosts []host
|
||||
type hostsMap map[string]host
|
|
@ -0,0 +1,14 @@
|
|||
package consul
|
||||
|
||||
type Repository interface {
|
||||
GetData() error
|
||||
ParseData() (hostsMap, error)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
LoadData() error
|
||||
GetHostnames() []string
|
||||
GetModifiedIndex(hostname string) int
|
||||
GetCreateIndex(hostname string) int
|
||||
GetMetaData(hostname string) map[string]string
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
r Repository
|
||||
hostsMap hostsMap
|
||||
}
|
||||
|
||||
//Generates new service instance based on the repo
|
||||
func NewService(repository Repository) Service {
|
||||
logrus.Debug("consul: NewService")
|
||||
return &service{r: repository}
|
||||
}
|
||||
|
||||
//Function calling underlying repository, retreive data and store as interface map
|
||||
func (s *service) LoadData() error {
|
||||
logrus.Debug("consul: LoadData")
|
||||
err := s.r.GetData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.hostsMap, err = s.r.ParseData()
|
||||
logrus.Debugf("%+v", s.hostsMap)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *service) GetHostnames() []string {
|
||||
logrus.Debug("consul: GetHostnames")
|
||||
hostnames := make([]string, 0)
|
||||
|
||||
for k := range s.hostsMap {
|
||||
hostnames = append(hostnames, k)
|
||||
}
|
||||
return hostnames
|
||||
|
||||
}
|
||||
|
||||
func (s *service) GetModifiedIndex(hostname string) int {
|
||||
return s.hostsMap[hostname].Service.ModifyIndex
|
||||
}
|
||||
|
||||
func (s *service) GetCreateIndex(hostname string) int {
|
||||
return s.hostsMap[hostname].Service.CreateIndex
|
||||
}
|
||||
|
||||
func (s *service) GetMetaData(hostname string) map[string]string {
|
||||
return s.hostsMap[hostname].Service.Meta
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type repo struct {
|
||||
rawData []byte
|
||||
}
|
||||
|
||||
//Create new file repository satisfying Repository interface
|
||||
func NewStdinRepository() Repository {
|
||||
logrus.Debug("consul: Creating stdin reader")
|
||||
return &repo{}
|
||||
}
|
||||
|
||||
//GetData - Load data from stdin and store in the memory
|
||||
func (r *repo) GetData() error {
|
||||
var err error
|
||||
logrus.Debug("consul: GetData: Opening stdin")
|
||||
|
||||
rawData, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
r.rawData = rawData
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
//ParseData - Parse loaded data and return as json.
|
||||
func (r *repo) ParseData() (hostsMap, error) {
|
||||
logrus.Debugf("consul: ParseData: Parsing Data")
|
||||
var hosts rawHosts
|
||||
if err := json.Unmarshal(r.rawData, &hosts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostsMap := make(hostsMap)
|
||||
for _, v := range hosts {
|
||||
hostsMap[v.Node.Node] = v
|
||||
}
|
||||
|
||||
return hostsMap, nil
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
module infra-sshfp-cf
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/cloudflare/cloudflare-go v0.41.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.13 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
github.com/cloudflare/cloudflare-go v0.40.0 h1:OjW+SYY7+NVSTj+/6kORqvu33LH7uZ0hUd/0qOqucxU=
|
||||
github.com/cloudflare/cloudflare-go v0.40.0/go.mod h1:MmAqiRfD8rjKEuUe4MYNHfHjYhFWfW7PNe12CCQWqPY=
|
||||
github.com/cloudflare/cloudflare-go v0.41.0 h1:4HiWuBpBj1fMiyWDfIlLGxnuPuEbkLi+SZWq9tgCFfc=
|
||||
github.com/cloudflare/cloudflare-go v0.41.0/go.mod h1:o0jm+vdFrhwy7GOT3PB/71JQ6kElUQcifPc2Z9KTxeE=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
@ -0,0 +1,84 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"infra-sshfp-cf/cloudflare"
|
||||
"infra-sshfp-cf/config"
|
||||
"infra-sshfp-cf/consul"
|
||||
"infra-sshfp-cf/sshfp"
|
||||
"infra-sshfp-cf/statestore"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
//Debug loglevel
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
|
||||
//Create configuration components
|
||||
cfgService := config.NewService(config.NewFileRepository())
|
||||
config, err := cfgService.LoadConfig("testcfg")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
//Create cloudflare components
|
||||
cloudflare := cloudflare.NewService(cloudflare.NewRepository(config.CloudflareToken, config.DomainName))
|
||||
|
||||
//Create STDIN Listener and start listening on (blocking operation)
|
||||
consul := consul.NewService(consul.NewStdinRepository())
|
||||
err = consul.LoadData()
|
||||
|
||||
//Code below is executed upon data receipt
|
||||
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
hosts := consul.GetHostnames()
|
||||
|
||||
//Open statestore
|
||||
statestore := statestore.NewService(statestore.NewMapRepository("statestore.json"))
|
||||
|
||||
//Iterate over hosts and check modify indexes
|
||||
for _, hostname := range hosts {
|
||||
modifiedIndex := consul.GetModifiedIndex(hostname)
|
||||
modified, _ := statestore.CheckIfModified(hostname, modifiedIndex)
|
||||
|
||||
if modified {
|
||||
//Check if hosts has A record in CF - if not ignore
|
||||
exists, err := cloudflare.FindHostByName(hostname)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
//Generate DNS records based on metadata
|
||||
sshfp := sshfp.NewService()
|
||||
consulKeys := sshfp.ParseConsulSSHRecords(consul.GetMetaData(hostname))
|
||||
|
||||
//GetCurrent configuration
|
||||
cloudflareKeys, err := cloudflare.GetSSHFPRecordsForHost(hostname)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
|
||||
configPlan := sshfp.PrepareConfiguration(hostname, cloudflareKeys, consulKeys)
|
||||
|
||||
//ConfigPlan is empty, but host was flagged as modified. It's very likely that is a new host, or db is corrupted
|
||||
if len(configPlan) == 0 {
|
||||
statestore.SaveState(hostname, modifiedIndex)
|
||||
}
|
||||
|
||||
if len(configPlan) > 0 {
|
||||
sshfp.PrintConfigPlan(configPlan)
|
||||
itemsApplied, err := cloudflare.ApplyConfigPlan(configPlan)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
if itemsApplied == len(configPlan) {
|
||||
statestore.SaveState(hostname, modifiedIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package sshfp
|
||||
|
||||
type SSHFPRecord struct {
|
||||
Algorithm string
|
||||
Type string
|
||||
Fingerprint string
|
||||
RecordID string
|
||||
}
|
||||
|
||||
type AlgorithmTypes map[string]string
|
||||
type HashTypes map[string]string
|
||||
|
||||
type ConfigPlanElement struct {
|
||||
Hostname string
|
||||
Operation Operation
|
||||
Record *SSHFPRecord
|
||||
}
|
||||
|
||||
type ConfigPlan []ConfigPlanElement
|
||||
|
||||
type Operation int
|
||||
|
||||
const (
|
||||
UPDATE Operation = iota
|
||||
CREATE
|
||||
DELETE
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package sshfp
|
||||
|
||||
type Service interface {
|
||||
ParseConsulSSHRecord(key string, value string) (*SSHFPRecord, error)
|
||||
ParseConsulSSHRecords(records map[string]string) []*SSHFPRecord
|
||||
PrepareConfiguration(hostname string, current []*SSHFPRecord, new []*SSHFPRecord) ConfigPlan
|
||||
PrintConfigPlan(configPlan ConfigPlan)
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
package sshfp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
Algorithms AlgorithmTypes
|
||||
Hashes HashTypes
|
||||
}
|
||||
|
||||
func NewService() Service {
|
||||
//Based on: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
|
||||
|
||||
hashes := make(HashTypes)
|
||||
hashes["sha1"] = "1"
|
||||
hashes["sha256"] = "2"
|
||||
|
||||
algorithms := make(AlgorithmTypes)
|
||||
algorithms["rsa"] = "1"
|
||||
algorithms["dsa"] = "2"
|
||||
algorithms["ecdsa"] = "3"
|
||||
algorithms["ed25519"] = "4"
|
||||
algorithms["ed449"] = "6"
|
||||
|
||||
return &service{Algorithms: algorithms, Hashes: hashes}
|
||||
|
||||
}
|
||||
|
||||
func (s *service) ParseConsulSSHRecord(key string, value string) (*SSHFPRecord, error) {
|
||||
logrus.Debugf("SSHFP: ParseConsulSSHRecord: %s %s", key, value)
|
||||
|
||||
output := &SSHFPRecord{}
|
||||
|
||||
key = strings.ToLower(key)
|
||||
value = strings.ToLower(value)
|
||||
|
||||
output.Fingerprint = value
|
||||
|
||||
splittedKey := strings.Split(key, "-")
|
||||
|
||||
if len(splittedKey) < 3 {
|
||||
return nil, errors.New("incorrect format")
|
||||
}
|
||||
|
||||
//Assumption: Last field is hash type
|
||||
output.Type = s.Hashes[splittedKey[len(splittedKey)-1]]
|
||||
|
||||
//Check first field - for "ecdsa" we can return value, for "ssh", we have to check second field.
|
||||
|
||||
switch splittedKey[0] {
|
||||
case "ecdsa":
|
||||
output.Algorithm = s.Algorithms[splittedKey[0]]
|
||||
|
||||
case "ssh":
|
||||
output.Algorithm = s.Algorithms[splittedKey[1]]
|
||||
|
||||
}
|
||||
|
||||
if output.Type == "" || output.Algorithm == "" || output.Fingerprint == "" {
|
||||
return nil, errors.New("cannot parse record")
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *service) ParseConsulSSHRecords(records map[string]string) []*SSHFPRecord {
|
||||
output := make([]*SSHFPRecord, 0)
|
||||
|
||||
for k, v := range records {
|
||||
sshRecord, err := s.ParseConsulSSHRecord(k, v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
output = append(output, sshRecord)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (s *service) PrepareConfiguration(hostname string, current []*SSHFPRecord, new []*SSHFPRecord) ConfigPlan {
|
||||
logrus.Debug("SSHFP: PrepareConfiguration")
|
||||
|
||||
/* Config plan have to cover the following scenarios:
|
||||
0. Both configs are empty - do nothing.
|
||||
1. New config is empty - DELETE everything
|
||||
2. Old config is empty - CREATE everything
|
||||
3. Record in new config doesn't exist in old config - CREATE record
|
||||
4. Record in new config has different fingerprint than the old one - UPDATE and remove from oldMap
|
||||
5. Record in new config and record in old config are the same - just remove from oldMap to avoid unwanted removal
|
||||
|
||||
Records left in oldMap should be qualified for removal
|
||||
*/
|
||||
|
||||
configPlan := make(ConfigPlan, 0)
|
||||
|
||||
// Scenario 0
|
||||
if len(current) == 0 && len(new) == 0 {
|
||||
logrus.Debug("Both configs are empty - do nothing")
|
||||
return configPlan
|
||||
}
|
||||
|
||||
//Scenario 1
|
||||
if len(current) > 0 && len(new) == 0 {
|
||||
logrus.Debug("New config is empty - remove config")
|
||||
for _, v := range current {
|
||||
configPlan = append(configPlan, ConfigPlanElement{Operation: DELETE, Record: v, Hostname: hostname})
|
||||
}
|
||||
return configPlan
|
||||
}
|
||||
|
||||
//Scenario 2
|
||||
if len(current) == 0 && len(new) > 0 {
|
||||
logrus.Debug("Old config is empty - create config")
|
||||
for _, v := range new {
|
||||
configPlan = append(configPlan, ConfigPlanElement{Operation: CREATE, Record: v, Hostname: hostname})
|
||||
}
|
||||
return configPlan
|
||||
}
|
||||
|
||||
//To handle scenarios 3-5 we have to create temporary maps with string "<algorithm><type>" as key.
|
||||
//Assumption: Pair algorithm+hash type is unique
|
||||
|
||||
oldMap := make(map[string]*SSHFPRecord)
|
||||
newMap := make(map[string]*SSHFPRecord)
|
||||
|
||||
//Create temporary maps for better searching
|
||||
for _, v := range current {
|
||||
oldMap[v.Algorithm+v.Type] = v
|
||||
}
|
||||
|
||||
for _, v := range new {
|
||||
newMap[v.Algorithm+v.Type] = v
|
||||
}
|
||||
|
||||
for k := range newMap {
|
||||
//Scenario 3
|
||||
if _, ok := oldMap[k]; !ok {
|
||||
logrus.Debugf("Record not found in current config: %s", k)
|
||||
configPlan = append(configPlan, ConfigPlanElement{Operation: CREATE, Record: newMap[k], Hostname: hostname})
|
||||
continue
|
||||
}
|
||||
//Scenario 4
|
||||
if oldMap[k].Fingerprint != newMap[k].Fingerprint {
|
||||
logrus.Debugf("Updating record in current config: %s", k)
|
||||
newMap[k].RecordID = oldMap[k].RecordID
|
||||
configPlan = append(configPlan, ConfigPlanElement{Operation: UPDATE, Record: newMap[k], Hostname: hostname})
|
||||
delete(oldMap, k)
|
||||
continue
|
||||
}
|
||||
//Scenario 5
|
||||
if oldMap[k].Fingerprint == newMap[k].Fingerprint {
|
||||
delete(oldMap, k)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
//Cleanup
|
||||
for _, v := range oldMap {
|
||||
configPlan = append(configPlan, ConfigPlanElement{Operation: DELETE, Record: v, Hostname: hostname})
|
||||
}
|
||||
return configPlan
|
||||
}
|
||||
func (s *service) PrintConfigPlan(configPlan ConfigPlan) {
|
||||
logrus.Debug("SSHFP: PrintConfigPlan")
|
||||
logrus.Infof("Config Plan:")
|
||||
for _, v := range configPlan {
|
||||
logrus.Infof("Hostname: %s", v.Hostname)
|
||||
logrus.Infof("Operation: %v", v.Operation)
|
||||
logrus.Infof("Record: %+v", v.Record)
|
||||
logrus.Infof("---")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package statestore
|
||||
|
||||
type Repository interface {
|
||||
GetModifyIndex(hostname string) (int, error)
|
||||
SetModifyIndex(hostname string, index int) error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
CheckIfModified(hostname string, index int) (bool, error)
|
||||
SaveState(hostname string, index int) error
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package statestore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type mapRepository struct {
|
||||
db map[string]int
|
||||
filename string
|
||||
}
|
||||
|
||||
func NewMapRepository(filename string) Repository {
|
||||
repo := new(mapRepository)
|
||||
repo.filename = filename
|
||||
|
||||
err := repo.openDatabase()
|
||||
if err == nil {
|
||||
return repo
|
||||
}
|
||||
|
||||
db := make(map[string]int)
|
||||
repo.db = db
|
||||
|
||||
err = repo.saveDatabase()
|
||||
if err != nil {
|
||||
logrus.Error("mapRepository: cannot save database %s", filename)
|
||||
}
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func (r *mapRepository) GetModifyIndex(hostname string) (int, error) {
|
||||
if value, ok := r.db[hostname]; ok {
|
||||
return value, nil
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (r *mapRepository) SetModifyIndex(hostname string, index int) error {
|
||||
r.db[hostname] = index
|
||||
return r.saveDatabase()
|
||||
}
|
||||
|
||||
func (r *mapRepository) openDatabase() error {
|
||||
logrus.Infof("mapRepository: openDatabase %s", r.filename)
|
||||
content, err := ioutil.ReadFile(r.filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(content, &r.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *mapRepository) saveDatabase() error {
|
||||
logrus.Infof("mapRepository: saveDatabase %s", r.filename)
|
||||
content, err := json.MarshalIndent(&r.db, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(r.filename, content, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package statestore
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
type service struct {
|
||||
r Repository
|
||||
}
|
||||
|
||||
func NewService(r Repository) Service {
|
||||
return &service{r: r}
|
||||
}
|
||||
|
||||
// CheckIfModified - returns if host is modified or not. Err can be ignored or not depends on repository
|
||||
func (s *service) CheckIfModified(hostname string, index int) (bool, error) {
|
||||
logrus.Debugf("statestore: CheckIfModified %s", hostname)
|
||||
indexDb, err := s.r.GetModifyIndex(hostname)
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if indexDb == index {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (s *service) SaveState(hostname string, index int) error {
|
||||
return s.r.SetModifyIndex(hostname, index)
|
||||
|
||||
}
|
Loading…
Reference in New Issue