Not too long ago I gave a presentation at a tech conference regarding using DNS blacklisting/blackholes/sinkholes to identify and mitigate the effects of malware. Specifically, it was that presentation that sparked my last blog post on configuring BIND to use DLZs. After that presentation I received a phone call from an entity wanting to use DLZs for their entire domain, not just for their DNS blackhole. Since my DNS servers already have to do at least one database lookup for every DNS query, and since I already have instructions on how to build a DNS blackhole using DLZs, I thought it fitting to go ahead and extend that to the logical ending -- using DLZs for an entire domain.
Database Changes
Since I already have a working PostGreSQL, BIND and DLZ deployment in a virtual environment, I'm going to use that as my workspace. Using either "\d" from the psql CLI or looking at the output of a "pg_dump -s dnsbh" from the system CLI shows the following existing columns for the dns_records table:
- id serial
- zone text
- host text
- ttl integer
- type text
- data text
To return "proper" responses for all queries related to a domain, there are some columns that need to be added:
- mx_priority, the priority for MX records
- contact, the responsible contact for the zone
- update_serial, the serial number for the zone update
- refresh, the amount of time slaves or secondary servers wait between update requests
- retry, the amount of time slaves or secondary servers wait between update requests after a failed request
- expiration, the amount of time before a record in the cache is considered stale
- minimum, the minimum amount of time to keep the item in the cache
- primary_ns, the DNS servers for the zone
ALTER TABLE dns_records
ADD COLUMN mx_priority INTEGER,
ADD COLUMN contact CHARACTER VARYING (255),
ADD COLUMN update_serial INTEGER,
ADD COLUMN refresh INTEGER,
ADD COLUMN retry INTEGER,
ADD COLUMN expiration INTEGER,
ADD COLUMN minimum INTEGER,
ADD COLUMN primary_ns CHARACTER VARYING (255);
Supporting the DLZ driver functions
The PostGreSQL DLZ driver expects some value (even if that value is "NULL") to be returned for the MX priority in its lookup() function as the third value returned, between 'type' and 'data'. This function performs the query defined by the seconds "SELECT" statement in the BIND configuration file. Those two things combined means that I need to add the mx_priority column to the SELECT query. I will post the entire BIND configuration later so don't worry just yet about editing the file, just be cognisant that it will have to happen.
The PostGreSQL DLZ driver needs to be able to query for SOA and NS records and it does this via the authority() function. The query for this function should return values for ttl, type, mx_priority, data, contact, update_serial, refresh, retry, expiration and minimum. This will get added after the existing final "SELECT" statement in the BIND configuration. There is a caveat to this - the query used by the lookup() function *CAN* be written to return SOA and NS records; if this is the case then the query for the authority() function can be an "empty query" and written as "{}" (note there are no spaces between the braces). I am specifically NOT letting my lookup() function pull NS and SOA records so a query is necessary for the authority() function.
The next item added to the BIND configuration is a query that returns values for the DLZ driver's allnodes() function. It will return values identical to the authority() function but the query doesn't differentiate between NS/SOA and other records so it will be nearly identical to the query written for the authority() function.
Finally, it is possible to add a query that supports the allowzonexfr() function and returns the zone and IP addresses of clients allowed to perform zone transfers for that zone. Note that if you want to allow zone transfers then you *must* support both the authority() and allowzonexfr() functions. Since part of the point of database replication and backing BIND with a database is to allow instant access to updates, I will NOT add a query to support this functionality. It is recommended against by the original bind-dlz team, for what I believe to be very good reasons, and I believe the proper way to handle those updates is via database replication to all secondary DNS servers. Using this model effectively removes the "master/slave" relationship and allows any DNS server in the organisation to act as a primary DNS server without having to change anything in the BIND configuration.
Taking all of the above modifications into consideration, the new dlz section of my BIND configuration now looks like this (on my production machines the select statements aren't so separated, I just do it here for clarity):
dlz "postgres" {
database "postgres 4
{host=localhost port=5432 dbname=dnsbh user=dnsbh password='password_you_used'}
{
select
zone
from
dns_records
where
zone = '$zone$'
}
{
select
ttl,
type,
mx_priority,
case when lower(type)='txt' then '\"' || data || '\"' else data end
from
dns_records
where
zone = '$zone$' and
host = '$record$' and
not (type = 'SOA' or type = 'NS')
}
{
select
ttl,
type,
data,
primary_ns,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$' and
(type = 'SOA' or type='NS')
}
{
select
ttl,
type,
host,
mx_priority,
data,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$'
}";
};
I decided to test with a very simple domain. At the minimum I wanted a primary nameserver, a mail server, a random machine and something to justify a CNAME record so I could perform the following lookups:
First, I'll add the SOA records for the demo.local zone and for reverse lookups for the 10.10.10.0/24 network:
The next item added to the BIND configuration is a query that returns values for the DLZ driver's allnodes() function. It will return values identical to the authority() function but the query doesn't differentiate between NS/SOA and other records so it will be nearly identical to the query written for the authority() function.
Finally, it is possible to add a query that supports the allowzonexfr() function and returns the zone and IP addresses of clients allowed to perform zone transfers for that zone. Note that if you want to allow zone transfers then you *must* support both the authority() and allowzonexfr() functions. Since part of the point of database replication and backing BIND with a database is to allow instant access to updates, I will NOT add a query to support this functionality. It is recommended against by the original bind-dlz team, for what I believe to be very good reasons, and I believe the proper way to handle those updates is via database replication to all secondary DNS servers. Using this model effectively removes the "master/slave" relationship and allows any DNS server in the organisation to act as a primary DNS server without having to change anything in the BIND configuration.
Editing the BIND configuration
1) Adding mx_priority for the lookup() function
{
select
ttl,
type,
mx_priority,
select
ttl,
type,
mx_priority,
case when lower(type)='txt' then '\"' || data || '\"' else data end
from
dns_records
dns_records
where
zone = '$zone$' and
zone = '$zone$' and
host = '$record$' and
not (type = 'SOA' or type = 'NS')}
}
2) Adding a query for the authority() function
{
select
ttl,
type,
data,
primary_ns,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$' and
(type = 'SOA' or type='NS')
}
select
ttl,
type,
data,
primary_ns,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$' and
(type = 'SOA' or type='NS')
}
3) Adding a query for the allnodes() function
{
select
ttl,
type,
host,
mx_priority,
data,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$'
}
select
ttl,
type,
host,
mx_priority,
data,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$'
}
Put all the queries together
Taking all of the above modifications into consideration, the new dlz section of my BIND configuration now looks like this (on my production machines the select statements aren't so separated, I just do it here for clarity):
dlz "postgres" {
database "postgres 4
{host=localhost port=5432 dbname=dnsbh user=dnsbh password='password_you_used'}
{
select
zone
from
dns_records
where
zone = '$zone$'
}
{
select
ttl,
type,
mx_priority,
case when lower(type)='txt' then '\"' || data || '\"' else data end
from
dns_records
where
zone = '$zone$' and
host = '$record$' and
not (type = 'SOA' or type = 'NS')
}
{
select
ttl,
type,
data,
primary_ns,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$' and
(type = 'SOA' or type='NS')
}
{
select
ttl,
type,
host,
mx_priority,
data,
contact,
update_serial,
refresh,
retry,
expiration,
minimum
from
dns_records
where
zone = '$zone$'
}";
};
Adding domain data to the database
I decided to test with a very simple domain. At the minimum I wanted a primary nameserver, a mail server, a random machine and something to justify a CNAME record so I could perform the following lookups:
- SOA
- NS
- A
- CNAME
- MX
- PTR
- 10.10.10.103 -- ns1.demo.local
- 10.10.10.112 -- mail.demo.local
First, I'll add the SOA records for the demo.local zone and for reverse lookups for the 10.10.10.0/24 network:
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('demo.local', '@', '86400', 'SOA', NULL, NULL, 'hostmaster.demo.local.', '2013010801', '3600', '1800', '604800', '86400', 'ns1.domain.local.');The NS records for my zone and IP range:
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('10.10.10.in-addr.arpa', '@', '86400', 'SOA', NULL, NULL, 'hostmaster.demo.local.', '2013010801', '3600', '1800', '604800', '86400', 'ns1.domain.local.');
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('demo.local', '@', '86400', 'NS', 'ns1.demo.local.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);The MX record with a priority of 10 for mail.demo.local:
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('10.10.10.in-addr.arpa', '@', '86400', 'NS', 'ns1.demo.local.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('demo.local', '@', '300', 'MX', 'mail.demo.local.', '10', NULL, NULL, '3600', '1800', '604800', '86400', NULL);The A records for ns1.demo.local (10.10.10.103) and mail.demo.local (10.10.10.112):
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('demo.local', 'ns1', '86400', 'A', '10.10.10.103', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);The PTR records so reverse lookups work:
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('demo.local', 'mail', '86400', 'A', '10.10.10.112', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('10.10.10.in-addr.arpa', '103', '86400', 'PTR', 'ns1.demo.local.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);Finally, the CNAME to point imap.demo.local to mail.demo.local:
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('10.10.10.in-addr.arpa', '112', '86400', 'PTR', 'mail.demo.local.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO dns_records (zone, host, ttl, type, data, mx_priority, contact, update_serial, refresh, retry, expiration, minimum, primary_ns) VALUES ('demo.local', 'imap', '86400', 'CNAME', 'mail.demo.local.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);At this point the following lookups should work when performed against the DNS server:
- dig soa demo.local
- dig ns ns1.demo.local
- dig mx demo.local
- dig mail.demo.local
- dig imap.demo.local
No comments:
Post a Comment
Note: only a member of this blog may post a comment.