Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81ca1810f1 | |||
| 6c5f06a3ff | |||
| f92ac4ebd8 | |||
| 49d9f0556c | |||
| b58dc9b065 |
10 changed files with 306 additions and 83 deletions
22
.drone-manifest.yml
Normal file
22
.drone-manifest.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
image: happydomain/checker-dnssec:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
- image: happydomain/checker-dnssec:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: happydomain/checker-dnssec:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
- image: happydomain/checker-dnssec:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
187
.drone.yml
Normal file
187
.drone.yml
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: checker build
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: checker build tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_SEMVER}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-dnssec
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub (tag)
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-dnssec
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_SEMVER}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-arm64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: checker build
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: checker build tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_SEMVER}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-dnssec
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub (tag)
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-dnssec
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_SEMVER}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: docker-manifest
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: .drone-manifest.yml
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
42
README.md
42
README.md
|
|
@ -96,28 +96,26 @@ make test # tests
|
|||
|
||||
## Rules
|
||||
|
||||
Each rule emits a finding code. Severity may be affected by the options above.
|
||||
|
||||
| Code | Default severity | Condition |
|
||||
|------|-----------------|-----------|
|
||||
| `dnssec_zone_signed` | critical | Parent DS is published but the apex serves no DNSKEY (broken chain of trust). |
|
||||
| `dnssec_dnskey_consistent` | critical | Authoritative servers disagree on the apex DNSKEY RRset. |
|
||||
| `dnssec_dnskey_query_ok` | warning | At least one authoritative server failed to answer the DNSKEY query. |
|
||||
| `dnssec_algorithm_allowed` | critical (forbidden) / warning (not in allowlist) | A DNSKEY uses a forbidden algorithm or an algorithm not in `allowedAlgorithms`. |
|
||||
| `dnssec_algorithm_modern` | warning | The zone still uses RSA-family DNSKEYs; ECDSAP256SHA256 (13) or Ed25519 (15) recommended. |
|
||||
| `dnssec_rsa_keysize` | critical (<1024) / warning (<`minRSAKeySize`) | An RSA DNSKEY has a modulus below the policy threshold. |
|
||||
| `dnssec_ksk_present` | critical | No DNSKEY carries the SEP (KSK) flag while `requireSEP` is enabled. |
|
||||
| `dnssec_dnskey_count` | warning | Eight or more DNSKEYs are published, bloating responses and amplification factor. |
|
||||
| `dnssec_rrsig_present_dnskey` | critical | The apex DNSKEY RRset is unsigned. |
|
||||
| `dnssec_rrsig_present_soa` | critical | The apex SOA RRset is unsigned. |
|
||||
| `dnssec_rrsig_validity_window` | critical | An observed RRSIG is outside its `[Inception, Expiration]` window. |
|
||||
| `dnssec_rrsig_freshness` | warning / critical | The closest RRSIG expires in fewer than `signatureFreshness` / `signatureFreshnessCrit` days. |
|
||||
| `dnssec_denial_uses_nsec3` | warning | The zone uses NSEC for denial of existence, exposing it to trivial walking (RFC 5155 / RFC 7129). |
|
||||
| `dnssec_nsec3_iterations` | warning / critical (per `nsec3IterationsSeverity`) | `NSEC3PARAM.Iterations` exceeds `nsec3IterationsMax` (RFC 9276 §3.1). |
|
||||
| `dnssec_nsec3_salt_empty` | warning | `NSEC3PARAM.SaltLength` is non-zero (RFC 9276 §3.1: a salt buys no measurable protection). |
|
||||
| `dnssec_nsec3_optout_only_when_signed_delegations` | info | The OPT-OUT flag is set in a leaf zone, where it serves no purpose. |
|
||||
| `dnssec_denial_consistent` | critical | Authoritative servers disagree on the denial-of-existence scheme (NSEC vs NSEC3, or differing parameters). |
|
||||
| `dnssec_dnskey_ttl_min` | warning | The DNSKEY TTL is below `dnskeyTTLMin`, hurting cache efficiency. |
|
||||
| Code | Description | Severity |
|
||||
|------------------------------------------------------|---------------------------------------------------------------------------------------------------|---------------------|
|
||||
| `dnssec_zone_signed` | Detects a zone advertised as signed at the parent (DS) but no DNSKEY served at the apex. | CRITICAL |
|
||||
| `dnssec_dnskey_consistent` | Verifies that every authoritative server returns the same DNSKEY RRset. | CRITICAL |
|
||||
| `dnssec_dnskey_query_ok` | Verifies that every authoritative server answered the DNSKEY query. | CRITICAL |
|
||||
| `dnssec_algorithm_allowed` | Rejects DNSKEYs that use a forbidden algorithm or are not in the allowed list. | CRITICAL |
|
||||
| `dnssec_algorithm_modern` | Recommends ECDSAP256SHA256 (13) or Ed25519 (15) over RSA. | WARNING |
|
||||
| `dnssec_rsa_keysize` | Verifies RSA DNSKEYs reach a minimum modulus size (default 2048 bits). | CRITICAL |
|
||||
| `dnssec_ksk_present` | Verifies at least one DNSKEY has the SEP bit (KSK). | CRITICAL |
|
||||
| `dnssec_dnskey_count` | Warns when too many DNSKEYs are published, inflating responses and amplification potential. | WARNING |
|
||||
| `dnssec_rrsig_present_dnskey` | Ensures the DNSKEY RRset is signed. | CRITICAL |
|
||||
| `dnssec_rrsig_present_soa` | Ensures the SOA RRset is signed. | CRITICAL |
|
||||
| `dnssec_rrsig_validity_window` | Verifies that every observed RRSIG is currently within [Inception, Expiration]. | CRITICAL |
|
||||
| `dnssec_rrsig_freshness` | Warns when RRSIGs are close to expiring; preemptive alert for stuck signers. | CRITICAL |
|
||||
| `dnssec_denial_uses_nsec3` | Warns when the zone uses NSEC for negative answers, which makes the zone walkable (RFC 5155 / RFC 7129). | WARNING |
|
||||
| `dnssec_nsec3_iterations` | Verifies that NSEC3PARAM.Iterations is at most nsec3IterationsMax (default 0, per RFC 9276 §3.1). | CRITICAL |
|
||||
| `dnssec_nsec3_salt_empty` | Verifies that NSEC3PARAM.SaltLength is 0 (RFC 9276 §3.1: a salt buys no measurable protection). | WARNING |
|
||||
| `dnssec_nsec3_optout_only_when_signed_delegations` | Reports informational note when the OPT-OUT flag is set on NSEC3PARAM in a leaf zone. | INFO |
|
||||
| `dnssec_denial_consistent` | Verifies that every authoritative server uses the same denial-of-existence scheme. | WARNING |
|
||||
| `dnssec_dnskey_ttl_min` | Warns when the DNSKEY TTL is too short to be useful for caching. | WARNING |
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ func collectFromServer(ctx context.Context, server, zone string) PerServerView {
|
|||
view.ProbeName = strings.TrimSuffix(probe, ".")
|
||||
if probeResp := authQuery(ctx, server, probe, dns.TypeA, &view, true); probeResp != nil {
|
||||
view.DenialKind, view.DenialRecords = classifyDenial(probeResp, view.NSEC3PARAM)
|
||||
} else if len(view.DNSKEYs) == 0 {
|
||||
} else if view.UDPError == "" && view.TCPError == "" && len(view.DNSKEYs) == 0 {
|
||||
view.DenialKind = DenialNone
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,54 +26,6 @@ func Definition() *sdk.CheckerDefinition {
|
|||
Description: "Recursive resolver used to discover the apex name servers and to look up the parent DS. Defaults to /etc/resolv.conf.",
|
||||
},
|
||||
},
|
||||
UserOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "nsec3IterationsMax",
|
||||
Type: "uint",
|
||||
Label: "Maximum NSEC3 iterations",
|
||||
Description: "RFC 9276 §3.1 sets the recommended ceiling at 0. Increase only if your signer cannot publish 0 yet.",
|
||||
Default: defaultNSEC3IterationsMax,
|
||||
},
|
||||
{
|
||||
Id: "nsec3IterationsSeverity",
|
||||
Type: "choice",
|
||||
Label: "Severity when NSEC3 iterations exceed the ceiling",
|
||||
Choices: []string{"warn", "crit"},
|
||||
Default: defaultNSEC3IterationsSeverityWarn,
|
||||
Description: "Use 'crit' to enforce RFC 9276 strictly.",
|
||||
},
|
||||
{
|
||||
Id: "signatureFreshness",
|
||||
Type: "uint",
|
||||
Label: "RRSIG freshness WARN threshold (days)",
|
||||
Description: "Warn when the closest RRSIG expires in fewer than this many days.",
|
||||
Default: defaultSignatureFreshnessDays,
|
||||
},
|
||||
{
|
||||
Id: "signatureFreshnessCrit",
|
||||
Type: "uint",
|
||||
Label: "RRSIG freshness CRIT threshold (days)",
|
||||
Default: defaultSignatureFreshnessCrit,
|
||||
},
|
||||
{
|
||||
Id: "minRSAKeySize",
|
||||
Type: "uint",
|
||||
Label: "Minimum RSA modulus size (bits)",
|
||||
Default: defaultMinRSAKeySize,
|
||||
},
|
||||
{
|
||||
Id: "requireSEP",
|
||||
Type: "bool",
|
||||
Label: "Require a KSK (DNSKEY with SEP bit)",
|
||||
Default: defaultRequireSEP,
|
||||
},
|
||||
{
|
||||
Id: "dnskeyTTLMin",
|
||||
Type: "uint",
|
||||
Label: "Minimum DNSKEY TTL (seconds)",
|
||||
Default: defaultDNSKEYTTLMinSec,
|
||||
},
|
||||
},
|
||||
DomainOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "domain_name",
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ type reportView struct {
|
|||
PerServer []serverView
|
||||
OtherFindings []otherFinding
|
||||
GlobalErrors []string
|
||||
RawJSON string
|
||||
}
|
||||
|
||||
type bannerView struct {
|
||||
|
|
@ -157,12 +156,6 @@ func buildReportView(d *DNSSECData, states []sdk.CheckState) *reportView {
|
|||
}
|
||||
v.GlobalErrors = d.Errors
|
||||
|
||||
if raw, err := json.MarshalIndent(d, "", " "); err == nil {
|
||||
v.RawJSON = string(raw)
|
||||
} else {
|
||||
v.GlobalErrors = append(v.GlobalErrors, fmt.Sprintf("render raw JSON: %v", err))
|
||||
}
|
||||
|
||||
v.Banner = buildBanner(d)
|
||||
v.Enumerability = buildEnum(d)
|
||||
v.Keys = buildKeys(d)
|
||||
|
|
@ -746,10 +739,5 @@ const reportTemplate = `<!DOCTYPE html>
|
|||
</table>
|
||||
{{end}}
|
||||
|
||||
{{if .RawJSON}}
|
||||
<h2>Raw observation</h2>
|
||||
<details><summary>Show JSON</summary><pre>{{.RawJSON}}</pre></details>
|
||||
{{end}}
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
|
|
|
|||
|
|
@ -102,6 +102,27 @@ func (nsec3IterationsRule) Name() string { return "dnssec_nsec3_iterations" }
|
|||
func (nsec3IterationsRule) Description() string {
|
||||
return "Verifies that NSEC3PARAM.Iterations is at most nsec3IterationsMax (default 0, per RFC 9276 §3.1)."
|
||||
}
|
||||
func (nsec3IterationsRule) Options() sdk.CheckerOptionsDocumentation {
|
||||
return sdk.CheckerOptionsDocumentation{
|
||||
UserOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "nsec3IterationsMax",
|
||||
Type: "uint",
|
||||
Label: "Maximum NSEC3 iterations",
|
||||
Description: "RFC 9276 §3.1 sets the recommended ceiling at 0. Increase only if your signer cannot publish 0 yet.",
|
||||
Default: defaultNSEC3IterationsMax,
|
||||
},
|
||||
{
|
||||
Id: "nsec3IterationsSeverity",
|
||||
Type: "choice",
|
||||
Label: "Severity when NSEC3 iterations exceed the ceiling",
|
||||
Choices: []string{"warn", "crit"},
|
||||
Default: defaultNSEC3IterationsSeverityWarn,
|
||||
Description: "Use 'crit' to enforce RFC 9276 strictly.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (nsec3IterationsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadDNSSEC(ctx, obs)
|
||||
|
|
|
|||
|
|
@ -139,6 +139,18 @@ func (rsaKeySizeRule) Name() string { return "dnssec_rsa_keysize" }
|
|||
func (rsaKeySizeRule) Description() string {
|
||||
return "Verifies RSA DNSKEYs reach a minimum modulus size (default 2048 bits)."
|
||||
}
|
||||
func (rsaKeySizeRule) Options() sdk.CheckerOptionsDocumentation {
|
||||
return sdk.CheckerOptionsDocumentation{
|
||||
UserOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "minRSAKeySize",
|
||||
Type: "uint",
|
||||
Label: "Minimum RSA modulus size (bits)",
|
||||
Default: defaultMinRSAKeySize,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (rsaKeySizeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadDNSSEC(ctx, obs)
|
||||
|
|
@ -194,6 +206,18 @@ func (kskPresentRule) Name() string { return "dnssec_ksk_present" }
|
|||
func (kskPresentRule) Description() string {
|
||||
return "Verifies at least one DNSKEY has the SEP bit (KSK)."
|
||||
}
|
||||
func (kskPresentRule) Options() sdk.CheckerOptionsDocumentation {
|
||||
return sdk.CheckerOptionsDocumentation{
|
||||
UserOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "requireSEP",
|
||||
Type: "bool",
|
||||
Label: "Require a KSK (DNSKEY with SEP bit)",
|
||||
Default: defaultRequireSEP,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (kskPresentRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadDNSSEC(ctx, obs)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,25 @@ func (rrsigFreshnessRule) Name() string { return "dnssec_rrsig_freshness" }
|
|||
func (rrsigFreshnessRule) Description() string {
|
||||
return "Warns when RRSIGs are close to expiring; preemptive alert for stuck signers."
|
||||
}
|
||||
func (rrsigFreshnessRule) Options() sdk.CheckerOptionsDocumentation {
|
||||
return sdk.CheckerOptionsDocumentation{
|
||||
UserOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "signatureFreshness",
|
||||
Type: "uint",
|
||||
Label: "RRSIG freshness WARN threshold (days)",
|
||||
Description: "Warn when the closest RRSIG expires in fewer than this many days.",
|
||||
Default: defaultSignatureFreshnessDays,
|
||||
},
|
||||
{
|
||||
Id: "signatureFreshnessCrit",
|
||||
Type: "uint",
|
||||
Label: "RRSIG freshness CRIT threshold (days)",
|
||||
Default: defaultSignatureFreshnessCrit,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (rrsigFreshnessRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadDNSSEC(ctx, obs)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ func (dnskeyTTLMinRule) Name() string { return "dnssec_dnskey_ttl_min" }
|
|||
func (dnskeyTTLMinRule) Description() string {
|
||||
return "Warns when the DNSKEY TTL is too short to be useful for caching."
|
||||
}
|
||||
func (dnskeyTTLMinRule) Options() sdk.CheckerOptionsDocumentation {
|
||||
return sdk.CheckerOptionsDocumentation{
|
||||
UserOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "dnskeyTTLMin",
|
||||
Type: "uint",
|
||||
Label: "Minimum DNSKEY TTL (seconds)",
|
||||
Default: defaultDNSKEYTTLMinSec,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (dnskeyTTLMinRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadDNSSEC(ctx, obs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue