11 Commits

Author SHA1 Message Date
12879324c6 Add aliases to readme 2021-06-19 15:58:35 +01:00
fc2031eb7c Support TLSA records
Closes: #4
2021-06-19 15:49:54 +01:00
17ff01d066 Handle SSHFP records
Closes #2
2021-06-19 15:42:11 +01:00
7a3e5e472a fix readme 2021-06-16 17:03:15 +01:00
95f0148af9 Implement dry-run 2021-06-16 17:01:01 +01:00
405903f0f5 Basic readme 2021-06-16 17:00:53 +01:00
4427f42e63 Implement CAA record
Docs say caa_property but implementation uses caa_tag (support both)

Ref: #3
2021-06-12 17:31:33 +01:00
401db7bda8 Support SRV records
Closes #1
2021-06-12 17:15:22 +01:00
dc1a90014a Enable support for simple types 2021-06-12 17:06:28 +01:00
07dc0d75c5 Handle unknown types a little nicer 2021-06-12 17:02:15 +01:00
965c94a223 Finalise the script autorun in docker 2021-06-12 17:00:01 +01:00
3 changed files with 179 additions and 21 deletions

View File

@ -2,3 +2,6 @@ FROM alpine
# docker build -t fooflington/mythic-beasts-dns . # docker build -t fooflington/mythic-beasts-dns .
LABEL maintainer="Matthew Slowe <foo@mafoo.org.uk>" LABEL maintainer="Matthew Slowe <foo@mafoo.org.uk>"
RUN apk --no-cache add perl perl-yaml perl-lwp-protocol-https perl-json perl-uri perl-yaml-tiny perl-www-form-urlencoded RUN apk --no-cache add perl perl-yaml perl-lwp-protocol-https perl-json perl-uri perl-yaml-tiny perl-www-form-urlencoded
COPY manage-dns.pl /docker-entry-point.pl
ENTRYPOINT ["perl", "/docker-entry-point.pl"]

93
README.md Normal file
View File

@ -0,0 +1,93 @@
# Mythic Beasts DNS manager
A simple management agent for controlling your DNS entries on the Mythic Beasts platform using a (version-controllable) YAML file.
This client uses the DNSv2 API (https://www.mythic-beasts.com/support/api/dnsv2) for which you'll need to create a key (https://www.mythic-beasts.com/customer/api-users).
## Configuration
1. Create an API key via https://www.mythic-beasts.com/customer/api-users
2. Create a YAML file
```yaml
defaults:
ttl:
example.com: 3600
api: https://api.mythic-beasts.com/dns/v2/zones
api_host: api.mythic-beasts.com:443
realm: Mythic Beasts DNS API
auth:
key: mykey
secret: mysecret
zones:
example.com:
```
3. Under `zones -> example.com`, prepare your DNS entries using the schema:
```yaml
zones:
example.com:
name1:
TYPE: value
name2:
TYPE:
- value1
- value2
name3.subdomain:
TYPE: value
```
Currently only a single zone is supported.
## Supported types
The client currently supports all of the Record Types implemented by the Mythic Beasts API except `SSHFP` and `TLSA`.
## Value syntax
The value for simple record types is the plain value as expected (eg. `A: 10.54.22.9` or `AAAA: 2a01:332::2`).
The value for complex types, such as `MX` is as per the standard zone file (eg. `MX: 10 mta.example.com`) At some point this will become parametrised.
## The Root object (`@`)
To refer to the base/root domain, use the `"@"` key:
```yaml
zones:
example.com:
"@":
A: 10.54.22.9
AAAA: 2a01:332::2
MX:
- 10 mta1.example.com
- 10 mta2.example.com
```
## Aliases virtual type
There is a virtual record type `aliases` which is a list of names to CNAME to this record.
For example:
```yaml
zones:
example.com:
"@":
A: 10.54.22.9
aliases:
- www
- ftp
```
# Running
Invoke the docker container with the input yaml file:
```bash
docker run --rm -ti -v "${PWD}:/a" -w /a fooflington/mythic-dns mafoo.org.uk.yml
```
## Dry run
Pass the environment variable `DRY_RUN` to prevent any changes:
```bash
docker run --rm -ti -v "${PWD}:/a" -w /a -e DRY_RUN=1 fooflington/mythic-dns mafoo.org.uk.yml
```

104
manage-dns.pl Normal file → Executable file
View File

@ -1,6 +1,7 @@
#!/usr/bin/env perl -w #!/usr/bin/perl
use strict; use strict;
use warnings;
use LWP::UserAgent; use LWP::UserAgent;
use Data::Dumper; use Data::Dumper;
@ -12,6 +13,7 @@ use WWW::Form::UrlEncoded::PP qw/build_urlencoded/;
my $in = YAML::Tiny->read(shift); my $in = YAML::Tiny->read(shift);
my $ua = LWP::UserAgent->new; my $ua = LWP::UserAgent->new;
my %seen; my %seen;
my $DRY_RUN = $ENV{DRY_RUN};
sub _debug { sub _debug {
print STDERR ("=== DEBUG ===\n", Dumper(@_), "=== END ===\n") if $ENV{DEBUG} or $in->[0]->{debug}; print STDERR ("=== DEBUG ===\n", Dumper(@_), "=== END ===\n") if $ENV{DEBUG} or $in->[0]->{debug};
@ -32,15 +34,15 @@ sub _notice {
my %supported_types = ( my %supported_types = (
A => "yes", A => "yes",
AAAA => "yes", AAAA => "yes",
CAA => "not yet implemented", CAA => "yes",
CNAME => "yes", CNAME => "yes",
DNAME => "not yet implemented", DNAME => "yes",
MX => "yes", MX => "yes",
NS => "yes", NS => "yes",
PTR => "not yet implemented", PTR => "yes",
SSHFP => "not yet implemented", SSHFP => "yes",
SRV => "yes", SRV => "yes",
TLSA => "not yet implemented", TLSA => "yes",
TXT => "yes", TXT => "yes",
); );
sub is_unsupported($) { sub is_unsupported($) {
@ -50,6 +52,8 @@ sub is_unsupported($) {
return $supported_types{$type}; return $supported_types{$type};
} }
_notice("Dry run - no changes will be applied") if ${DRY_RUN};
if($ENV{DEBUG} or $in->[0]->{debug}) { if($ENV{DEBUG} or $in->[0]->{debug}) {
use LWP::Debug qw(+); use LWP::Debug qw(+);
$ua->add_handler( $ua->add_handler(
@ -119,6 +123,30 @@ sub format_record($$$$) {
my ($pri, $data) = split(/\s+/, $value); my ($pri, $data) = split(/\s+/, $value);
$record->{mx_priority} = $pri; $record->{mx_priority} = $pri;
$record->{data} = $data; $record->{data} = $data;
} elsif ($type eq 'SRV') {
# pri weight port data
my ($pri, $weight, $port, $data) = split(/\s+/, $value);
$record->{srv_priority} = $pri;
$record->{srv_weight} = $weight;
$record->{srv_port} = $port;
$record->{data} = $data;
} elsif ($type eq 'CAA') {
my ($flags, $property, $data) = split(/\s+/, $value);
$record->{caa_flags} = $flags;
$record->{caa_property} = $property;
$record->{caa_tag} = $property;
$record->{data} = $data;
} elsif ($type eq 'SSHFP') {
my ($algo, $keytype, $data) = split(/\s+/, $value);
$record->{sshfp_type} = $keytype;
$record->{sshfp_algorithm} = $algo;
$record->{data} = $data;
} elsif ($type eq 'TLSA') {
my ($usage, $selector, $matching, $data) = split(/\s+/, $value);
$record->{tlsa_usage} = $usage;
$record->{tlsa_selector} = $selector;
$record->{tlsa_matching} = $matching;
$record->{data} = $data;
} }
return $record; return $record;
@ -128,6 +156,32 @@ sub reformat_data($$) {
my ($type, $data) = @_; my ($type, $data) = @_;
if($type eq 'MX') { if($type eq 'MX') {
return sprintf('%d %s', $data->{mx_priority}, $data->{data}); return sprintf('%d %s', $data->{mx_priority}, $data->{data});
} elsif($type eq 'SRV') {
return sprintf('%d %d %d %s',
$data->{srv_priority},
$data->{srv_weight},
$data->{srv_port},
$data->{data}
);
} elsif($type eq 'CAA') {
return sprintf('%d %s %s',
$data->{caa_flags},
$data->{caa_property} || $data->{caa_tag},
$data->{data}
);
} elsif($type eq 'SSHFP') {
return sprintf('%d %d %s',
$data->{sshfp_algorithm},
$data->{sshfp_type},
$data->{data},
);
} elsif($type eq 'TLSA') {
return sprintf('%d %d %d %s',
$data->{tlsa_usage},
$data->{tlsa_selector},
$data->{tlsa_matching},
$data->{data},
);
} }
return $data->{data}; return $data->{data};
@ -136,7 +190,8 @@ sub reformat_data($$) {
sub check_and_update_record($$$$$) { sub check_and_update_record($$$$$) {
my ($zone, $data, $type, $host, $value) = @_; my ($zone, $data, $type, $host, $value) = @_;
if(my $err = is_unsupported($type)) { if(my $err = is_unsupported($type)) {
die ("Unable to process $host $type: $err"); warn ("WARNING: Unable to process $host $type: $err");
return;
} }
# _info("Considering %s %s %s", $host, $type, $value); # _info("Considering %s %s %s", $host, $type, $value);
@ -150,30 +205,36 @@ sub check_and_update_record($$$$$) {
# Update the record # Update the record
$record->{ttl} = $in->[0]->{defaults}->{ttl}->{$zone}; $record->{ttl} = $in->[0]->{defaults}->{ttl}->{$zone};
_debug("Update ", $url, $record, to_json($record)); _debug("Update ", $url, $record, to_json($record));
my $res = $ua->put( unless ($DRY_RUN) {
$url, my $res = $ua->put(
"Content-Type" => "application/json", $url,
"Content" => to_json({ records => [ $record ] }), "Content-Type" => "application/json",
); "Content" => to_json({ records => [ $record ] }),
warn "Failed to update $url: " . $res->status_line unless $res->is_success; );
warn "Failed to update $url: " . $res->status_line unless $res->is_success;
}
} }
} else { } else {
# Create new record # Create new record
my $new = format_record($zone, $type, $host, $value); my $new = format_record($zone, $type, $host, $value);
_notice("Created new record: %s %s %s", $host, $type, $value); _notice("Created new record: %s %s %s", $host, $type, $value);
my $res = $ua->post( _debug($new);
$url, unless ($DRY_RUN) {
"Content-Type" => "application/json", my $res = $ua->post(
Content => to_json({ $url,
records => [ $new ] "Content-Type" => "application/json",
}) Content => to_json({
); records => [ $new ]
warn "Failed to create $url: " . $res->status_line . "\n" . $res->content unless $res->is_success; })
);
warn "Failed to create $url: " . $res->status_line . "\n" . $res->content unless $res->is_success;
}
} }
} }
sub delete_record($$) { sub delete_record($$) {
my ($zone, $record) = @_; my ($zone, $record) = @_;
return if $DRY_RUN;
my $url = $in->[0]->{defaults}->{api} . "/$zone/records/$record->{host}/$record->{type}?host=$record->{host}&data=$record->{data}"; my $url = $in->[0]->{defaults}->{api} . "/$zone/records/$record->{host}/$record->{type}?host=$record->{host}&data=$record->{data}";
my $res = $ua->delete($url); my $res = $ua->delete($url);
@ -215,6 +276,7 @@ foreach my $z (keys %{$in->[0]->{zones}}) {
unless (defined $seen{$record}) { unless (defined $seen{$record}) {
# _info("Considering %s %s", $record->{host}, $record->{type}); # _info("Considering %s %s", $record->{host}, $record->{type});
my $skip; my $skip;
$skip = 1 if is_unsupported($record->{type});
if ($in->[0]->{ignore}->{$z}->{$record->{host}}) { if ($in->[0]->{ignore}->{$z}->{$record->{host}}) {
# check if type is specified # check if type is specified
if(keys %{$in->[0]->{ignore}->{$z}->{$record->{host}}}) { if(keys %{$in->[0]->{ignore}->{$z}->{$record->{host}}}) {