From 20d60098ea876a5fce5c2aebc455859fca65fca2 Mon Sep 17 00:00:00 2001
From: magior <2691798+magior@users.noreply.github.com>
Date: Tue, 17 Mar 2026 21:46:55 +0100
Subject: [PATCH 1/2] Add per-zone allow_transfer for primary zones
---
REFERENCE.md | 21 +++++++++++-
manifests/zone/primary.pp | 35 +++++++++++++------
spec/defines/zone/primary_spec.rb | 57 +++++++++++++++++++++++++++++++
templates/zone-primary.epp | 8 +++++
4 files changed, 109 insertions(+), 12 deletions(-)
diff --git a/REFERENCE.md b/REFERENCE.md
index 51a1456..44a2404 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -2472,6 +2472,16 @@ bind::zone::primary { 'example.com':
}
```
+##### Restrict zone transfers for a primary zone
+
+```puppet
+
+bind::zone::primary { 'example.com':
+ source => 'puppet:///modules/profile/example.com.zone',
+ allow_transfer => ['192.0.2.42'],
+}
+```
+
##### Use DNSSEC signing for a primary zone using a DNSSEC policy
```puppet
@@ -2499,6 +2509,7 @@ bind::zone::primary { '_acme-challenge.example.com':
The following parameters are available in the `bind::zone::primary` defined type:
* [`also_notify`](#-bind--zone--primary--also_notify)
+* [`allow_transfer`](#-bind--zone--primary--allow_transfer)
* [`update_policy`](#-bind--zone--primary--update_policy)
* [`dnssec_enable`](#-bind--zone--primary--dnssec_enable)
* [`dnssec_dnskey_kskonly`](#-bind--zone--primary--dnssec_dnskey_kskonly)
@@ -2530,6 +2541,15 @@ nameservers that are listed in the zone file.
Default value: `[]`
+##### `allow_transfer`
+
+Data type: `Array[String]`
+
+An array of ACL names or networks that are allowed to transfer zone
+information for this zone.
+
+Default value: `[]`
+
##### `update_policy`
Data type: `Variant[Enum['local'],Array[String]]`
@@ -3372,4 +3392,3 @@ Alias of `Enum['critical', 'error', 'warning', 'notice', 'info', 'debug', 'dynam
Type to match allowed values for the zone class
Alias of `Enum['IN', 'HS', 'CH']`
-
diff --git a/manifests/zone/primary.pp b/manifests/zone/primary.pp
index 2a4554e..62f48a0 100644
--- a/manifests/zone/primary.pp
+++ b/manifests/zone/primary.pp
@@ -13,6 +13,13 @@
# source => 'puppet:///modules/profile/example.com.zone',
# }
#
+# @example Restrict zone transfers for a primary zone
+#
+# bind::zone::primary { 'example.com':
+# source => 'puppet:///modules/profile/example.com.zone',
+# allow_transfer => ['192.0.2.42'],
+# }
+#
# @example Use DNSSEC signing for a primary zone using a DNSSEC policy
#
# bind::zone::primary { 'example.com':
@@ -33,6 +40,10 @@
# Secondary servers that should be notified in addition to the
# nameservers that are listed in the zone file.
#
+# @param allow_transfer
+# An array of ACL names or networks that are allowed to transfer zone
+# information for this zone.
+#
# @param update_policy
# Enable dynamic updates for the zone and define the update policy. This
# can either be the string `local` or an array of strings. Using the string
@@ -124,6 +135,7 @@
#
define bind::zone::primary (
Array[String] $also_notify = [],
+ Array[String] $allow_transfer = [],
Variant[Enum['local'],Array[String]] $update_policy = [],
Optional[Boolean] $dnssec_enable = undef,
Optional[Boolean] $dnssec_dnskey_kskonly = undef,
@@ -257,17 +269,18 @@
}
$params = {
- 'zone' => $zone,
- 'file' => $zonefile,
- 'also_notify' => $also_notify,
- 'notify' => $notify_secondaries,
- 'statistics' => $zone_statistics,
- 'update_policy' => $update_policy,
- 'class' => $class,
- 'comment' => $comment,
- 'indent' => bool2str($bind::views_enable, ' ', ''),
- 'zone_in_view' => ($view =~ NotUndef),
- 'dnssec_params' => !empty(delete_undef_values($params_dnssec)),
+ 'zone' => $zone,
+ 'file' => $zonefile,
+ 'also_notify' => $also_notify,
+ 'allow_transfer' => $allow_transfer,
+ 'notify' => $notify_secondaries,
+ 'statistics' => $zone_statistics,
+ 'update_policy' => $update_policy,
+ 'class' => $class,
+ 'comment' => $comment,
+ 'indent' => bool2str($bind::views_enable, ' ', ''),
+ 'zone_in_view' => ($view =~ NotUndef),
+ 'dnssec_params' => !empty(delete_undef_values($params_dnssec)),
}
if $bind::views_enable {
diff --git a/spec/defines/zone/primary_spec.rb b/spec/defines/zone/primary_spec.rb
index ca11b8a..27b5c5d 100644
--- a/spec/defines/zone/primary_spec.rb
+++ b/spec/defines/zone/primary_spec.rb
@@ -375,6 +375,44 @@
}
end
+ context 'with source => "/file", allow_transfer => ["any"]' do
+ let(:params) do
+ { source: '/file', allow_transfer: ['any'] }
+ end
+
+ it {
+ is_expected.to contain_file('/var/lib/bind/primary/com')
+ is_expected.to contain_file('/var/lib/bind/primary/com/example')
+ is_expected.to contain_file('/var/lib/bind/primary/com/example/db.example.com')
+
+ is_expected.to contain_exec('bind::reload::example.com')
+
+ is_expected.to contain_concat__fragment('named.conf.zones-example.com')
+ .with_target('named.conf.zones')
+ .with_order('20')
+ .with_content(%r{allow-transfer {\n\s+any;\n\s+};})
+ }
+ end
+
+ context 'with source => "/file", allow_transfer => ["acl1", "192.0.2.42"]' do
+ let(:params) do
+ { source: '/file', allow_transfer: ['acl1', '192.0.2.42'] }
+ end
+
+ it {
+ is_expected.to contain_file('/var/lib/bind/primary/com')
+ is_expected.to contain_file('/var/lib/bind/primary/com/example')
+ is_expected.to contain_file('/var/lib/bind/primary/com/example/db.example.com')
+
+ is_expected.to contain_exec('bind::reload::example.com')
+
+ is_expected.to contain_concat__fragment('named.conf.zones-example.com')
+ .with_target('named.conf.zones')
+ .with_order('20')
+ .with_content(%r{allow-transfer {\n\s+acl1;\n\s+192.0.2.42;\n\s+};})
+ }
+ end
+
context 'with source => "/file", zone_statistics => true' do
let(:params) do
{ source: '/file', zone_statistics: true }
@@ -815,6 +853,25 @@
}
end
+ context 'with view => "internal", source => "/file", allow_transfer => ["any"]' do
+ let(:params) do
+ { view: 'internal', source: '/file', allow_transfer: ['any'] }
+ end
+
+ it {
+ is_expected.to contain_file('/var/lib/bind/primary/com')
+ is_expected.to contain_file('/var/lib/bind/primary/com/example')
+ is_expected.to contain_file('/var/lib/bind/primary/com/example/db.example.com')
+
+ is_expected.to contain_exec('bind::reload::internal::example.com')
+
+ is_expected.to contain_concat__fragment('named.conf.views-internal-50-example.com')
+ .with_target('named.conf.views')
+ .with_order('10')
+ .with_content(%r{allow-transfer {\n \s+any;\n \s+};})
+ }
+ end
+
context 'with view => "internal", source => "/file", zone_statistics => true' do
let(:params) do
{ view: 'internal', source: '/file', zone_statistics: true }
diff --git a/templates/zone-primary.epp b/templates/zone-primary.epp
index 64f7e91..5c8d8a5 100644
--- a/templates/zone-primary.epp
+++ b/templates/zone-primary.epp
@@ -29,6 +29,14 @@
<% } -%>
<%= $indent %> };
<% } -%>
+<% unless empty($allow_transfer) { -%>
+
+<%= $indent %> allow-transfer {
+<% $allow_transfer.each |$item| { -%>
+<%= $indent %> <%= $item -%>;
+<% } -%>
+<%= $indent %> };
+<% } -%>
<% if $dnssec_params { -%>
<% if $dnssec_enable =~ NotUndef { -%>
From e0062761de9508d13d437921661eb6f237d6d76d Mon Sep 17 00:00:00 2001
From: magior <2691798+magior@users.noreply.github.com>
Date: Tue, 17 Mar 2026 22:35:31 +0100
Subject: [PATCH 2/2] Add custom view options support
---
README.md | 24 ++++++++++++++++++++++
REFERENCE.md | 15 ++++++++++++++
manifests/view.pp | 10 ++++++++++
spec/defines/view_spec.rb | 42 +++++++++++++++++++++++++++++++++++++++
templates/view.epp | 4 ++++
5 files changed, 95 insertions(+)
diff --git a/README.md b/README.md
index d5744ce..b73d37e 100644
--- a/README.md
+++ b/README.md
@@ -263,6 +263,30 @@ bind::views:
The defined types `bind::zone::primary` and `bind::zone::secondary` can be used to add zones to this view.
+Global options that are not modeled as dedicated class parameters can be
+set with `custom_options`.
+
+```puppet
+class { 'bind':
+ custom_options => {
+ 'minimal-responses' => 'no-auth-recursive',
+ },
+}
+```
+
+The `bind::view` defined type supports the same pattern for per-view options:
+
+```puppet
+bind::view { 'internal':
+ match_clients => [ 'localnets', ],
+ allow_query => [ 'localnets', ],
+ allow_recursion => [ 'localnets', ],
+ custom_options => {
+ 'minimal-responses' => true,
+ },
+}
+```
+
## Reference
See [REFERENCE.md](https://github.com/smoeding/puppet-bind/blob/master/REFERENCE.md)
diff --git a/REFERENCE.md b/REFERENCE.md
index 44a2404..f05aa1e 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -1997,6 +1997,7 @@ The following parameters are available in the `bind::view` defined type:
* [`view`](#-bind--view--view)
* [`order`](#-bind--view--order)
* [`response_policies`](#-bind--view--response_policies)
+* [`custom_options`](#-bind--view--custom_options)
##### `match_clients`
@@ -2156,6 +2157,19 @@ An array of response policy zones.
Default value: `[]`
+##### `custom_options`
+
+Data type: `Hash[String,Data]`
+
+Additional config options that are not implemented as parameters of this
+defined type can be set by a hash of custom options. Each key of the hash
+will be added to the view block of the configuration. For string or numeric
+values the value will be added as a normal option value. If the value is a
+hash or an array it will be included as an additional block enclosed in
+braces.
+
+Default value: `{}`
+
### `bind::zone::forward`
Manage a forward zone
@@ -3392,3 +3406,4 @@ Alias of `Enum['critical', 'error', 'warning', 'notice', 'info', 'debug', 'dynam
Type to match allowed values for the zone class
Alias of `Enum['IN', 'HS', 'CH']`
+
diff --git a/manifests/view.pp b/manifests/view.pp
index 80a4c43..77a7721 100644
--- a/manifests/view.pp
+++ b/manifests/view.pp
@@ -75,6 +75,14 @@
# @param response_policies
# An array of response policy zones.
#
+# @param custom_options
+# Additional config options that are not implemented as parameters of this
+# defined type can be set by a hash of custom options. Each key of the hash
+# will be added to the view block of the configuration. For string or numeric
+# values the value will be added as a normal option value. If the value is a
+# hash or an array it will be included as an additional block enclosed in
+# braces.
+#
#
define bind::view (
Array[String] $match_clients = ['any',],
@@ -93,6 +101,7 @@
String $view = $name,
String $order = '10',
Array[String] $response_policies = [],
+ Hash[String,Data] $custom_options = {},
Optional[Boolean] $localhost_forward_enable = undef,
Optional[Boolean] $localhost_reverse_enable = undef,
) {
@@ -120,6 +129,7 @@
'allow_query_cache_on' => $allow_query_cache_on,
'allow_transfer' => $allow_transfer,
'response_policies' => $response_policies,
+ 'custom_options' => bind::gencfg($custom_options, 2),
}
concat::fragment { "named.conf.views-${view}-00":
diff --git a/spec/defines/view_spec.rb b/spec/defines/view_spec.rb
index f54e7ff..aefc315 100644
--- a/spec/defines/view_spec.rb
+++ b/spec/defines/view_spec.rb
@@ -205,6 +205,48 @@
}
end
+ context 'with custom_options => { "minimal-responses" => true }' do
+ let(:params) do
+ { custom_options: { 'minimal-responses' => true } }
+ end
+
+ it {
+ is_expected.to contain_concat__fragment('named.conf.views-internal-00')
+ .with_target('named.conf.views')
+ .with_content("\nview \"internal\" {\n match-clients {\n any;\n };\n\n allow-query {\n any;\n };\n\n recursion yes;\n\n minimal-responses yes;\n")
+ .with_order('10')
+
+ is_expected.to contain_concat__fragment('named.conf.views-internal-99')
+ .with_content('};')
+ }
+ end
+
+ context 'with custom_options => { "minimal-responses" => "no-auth-recursive" }' do
+ let(:params) do
+ { custom_options: { 'minimal-responses' => 'no-auth-recursive' } }
+ end
+
+ it {
+ is_expected.to contain_concat__fragment('named.conf.views-internal-00')
+ .with_target('named.conf.views')
+ .with_content("\nview \"internal\" {\n match-clients {\n any;\n };\n\n allow-query {\n any;\n };\n\n recursion yes;\n\n minimal-responses no-auth-recursive;\n")
+ .with_order('10')
+ }
+ end
+
+ context 'with custom_options => { "sortlist" => ["localnets", "localhost"] }' do
+ let(:params) do
+ { custom_options: { 'sortlist' => ['localnets', 'localhost'] } }
+ end
+
+ it {
+ is_expected.to contain_concat__fragment('named.conf.views-internal-00')
+ .with_target('named.conf.views')
+ .with_content("\nview \"internal\" {\n match-clients {\n any;\n };\n\n allow-query {\n any;\n };\n\n recursion yes;\n\n sortlist {\n localnets;\n localhost;\n };\n")
+ .with_order('10')
+ }
+ end
+
context 'with root_hints_enable => true' do
let(:params) do
{ root_hints_enable: true }
diff --git a/templates/view.epp b/templates/view.epp
index 0a285a4..b9b6f60 100644
--- a/templates/view.epp
+++ b/templates/view.epp
@@ -83,3 +83,7 @@ view "<%= $view -%>" {
<% } -%>
};
<% } -%>
+<% unless empty($custom_options) { -%>
+
+<%= $custom_options -%>
+<% } -%>