Skip to content

Commit 536e329

Browse files
committed
feat(dnssec): support cryptokeys endpoint
This allows for optional management of DNSSEC. Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
1 parent 1432d76 commit 536e329

File tree

5 files changed

+166
-1
lines changed

5 files changed

+166
-1
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,23 @@ environments:
232232
global_tsigkeys: true
233233
```
234234

235+
#### CryptoKeys (DNSSEC)
236+
237+
Global or zone-specific CryptoKeys access can be enabled.
238+
239+
This allows for reading and writing of DNSSEC key material.
240+
241+
```yaml
242+
...
243+
environments:
244+
- name: "Test1"
245+
global_cryptokeys: true
246+
- name: example.com
247+
zones:
248+
- name: "example.com"
249+
cryptokeys: true
250+
```
251+
235252
### Metrics of the proxy
236253

237254
The proxy exposes metrics on the `/metrics` endpoint.

powerdns_api_proxy/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,20 @@ def check_acme_record_allowed(zone: ProxyConfigZone, rrset: RRSET) -> bool:
190190
return False
191191

192192

193+
def check_pdns_cryptokeys_allowed(
194+
environment: ProxyConfigEnvironment, zone: str
195+
) -> bool:
196+
if environment.global_cryptokeys:
197+
return True
198+
199+
try:
200+
return environment.get_zone_if_allowed(zone).cryptokeys
201+
except ZoneNotAllowedException:
202+
pass
203+
204+
return False
205+
206+
193207
def check_pdns_tsigkeys_allowed(environment: ProxyConfigEnvironment) -> bool:
194208
if environment.global_tsigkeys:
195209
return True

powerdns_api_proxy/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class ProxyConfigZone(BaseModel):
2727
`subzones` sets the same permissions on all subzones.
2828
`all_records` will be set to `True` if no `records` are defined.
2929
`read_only` will be set to `True` if `global_read_only` is `True`.
30+
`cryptokeys` enables management of DNSSEC.
3031
"""
3132

3233
name: str
@@ -39,6 +40,7 @@ class ProxyConfigZone(BaseModel):
3940
subzones: bool = False
4041
all_records: bool = False
4142
read_only: bool = False
43+
cryptokeys: bool = False
4244

4345
def __init__(self, **data):
4446
super().__init__(**data)
@@ -56,6 +58,7 @@ class ProxyConfigEnvironment(BaseModel):
5658
zones: list[ProxyConfigZone]
5759
global_read_only: bool = False
5860
global_search: bool = False
61+
global_cryptokeys: bool = False
5962
global_tsigkeys: bool = False
6063
_zones_lookup: dict[str, ProxyConfigZone] = {}
6164
metrics_proxy: bool = False

powerdns_api_proxy/proxy.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from powerdns_api_proxy.config import (
1515
check_pdns_search_allowed,
16+
check_pdns_cryptokeys_allowed,
1617
check_pdns_tsigkeys_allowed,
1718
check_pdns_zone_admin,
1819
check_pdns_zone_allowed,
@@ -479,6 +480,115 @@ async def search_data(
479480
return JSONResponse(content=pdns_response.data, status_code=status_code)
480481

481482

483+
@router_pdns.get("/servers/{server_id}/zones/{zone_id}/cryptokeys")
484+
async def list_cryptokeys(server_id: str, zone_id: str, X_API_Key: str = Header()):
485+
"""
486+
Get all CryptoKeys for a zone, except the private key.
487+
488+
<https://doc.powerdns.com/authoritative/http-api/cryptokey.html#get--servers-server_id-zones-zone_id-cryptokeys>
489+
"""
490+
environment = get_environment_for_token(config, X_API_Key)
491+
if not check_pdns_cryptokeys_allowed(environment, zone_id):
492+
logger.info(f"CryptoKeys not allowed for environment {environment.name}")
493+
raise ZoneNotAllowedException()
494+
resp = await pdns.get(f"/api/v1/servers/{server_id}/zones/{zone_id}/cryptokeys")
495+
pdns_response = await handle_pdns_response(resp)
496+
status_code = pdns_response.raise_for_error()
497+
return JSONResponse(content=pdns_response.data, status_code=status_code)
498+
499+
500+
@router_pdns.post("/servers/{server_id}/zones/{zone_id}/cryptokeys")
501+
async def create_cryptokey(
502+
request: Request, server_id: str, zone_id: str, X_API_Key: str = Header()
503+
):
504+
"""
505+
Creates a Cryptokey.
506+
507+
This method adds a new key to a zone.
508+
509+
<https://doc.powerdns.com/authoritative/http-api/cryptokey.html#post--servers-server_id-zones-zone_id-cryptokeys>
510+
"""
511+
environment = get_environment_for_token(config, X_API_Key)
512+
if not check_pdns_cryptokeys_allowed(environment, zone_id):
513+
logger.info(f"CryptoKeys not allowed for environment {environment.name}")
514+
raise ZoneNotAllowedException()
515+
resp = await pdns.post(
516+
f"/api/v1/servers/{server_id}/zones/{zone_id}/cryptokeys",
517+
payload=await request.json(),
518+
)
519+
pdns_response = await handle_pdns_response(resp)
520+
status_code = pdns_response.raise_for_error()
521+
return JSONResponse(content=pdns_response.data, status_code=status_code)
522+
523+
524+
@router_pdns.get("/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}")
525+
async def fetch_cryptokey(
526+
server_id: str, zone_id: str, cryptokey_id: str, X_API_Key: str = Header()
527+
):
528+
"""
529+
Returns all data about the CryptoKey, including the private key.
530+
531+
<https://doc.powerdns.com/authoritative/http-api/cryptokey.html#get--servers-server_id-zones-zone_id-cryptokeys-cryptokey_id>
532+
"""
533+
environment = get_environment_for_token(config, X_API_Key)
534+
if not check_pdns_cryptokeys_allowed(environment, zone_id):
535+
logger.info(f"CryptoKeys not allowed for environment {environment.name}")
536+
raise ZoneNotAllowedException()
537+
resp = await pdns.get(
538+
f"/api/v1/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}"
539+
)
540+
pdns_response = await handle_pdns_response(resp)
541+
status_code = pdns_response.raise_for_error()
542+
return JSONResponse(content=pdns_response.data, status_code=status_code)
543+
544+
545+
@router_pdns.put("/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}")
546+
async def update_cryptokey(
547+
request: Request,
548+
server_id: str,
549+
zone_id: str,
550+
cryptokey_id: str,
551+
X_API_Key: str = Header(),
552+
):
553+
"""
554+
This method (de)activates a key from zone_name specified by cryptokey_id.
555+
556+
<https://doc.powerdns.com/authoritative/http-api/cryptokey.html#put--servers-server_id-zones-zone_id-cryptokeys-cryptokey_id>
557+
"""
558+
environment = get_environment_for_token(config, X_API_Key)
559+
if not check_pdns_cryptokeys_allowed(environment, zone_id):
560+
logger.info(f"CryptoKeys not allowed for environment {environment.name}")
561+
raise ZoneNotAllowedException()
562+
resp = await pdns.put(
563+
f"/api/v1/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}",
564+
payload=await request.json(),
565+
)
566+
pdns_response = await handle_pdns_response(resp)
567+
status_code = pdns_response.raise_for_error()
568+
return JSONResponse(content=pdns_response.data, status_code=status_code)
569+
570+
571+
@router_pdns.delete("/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}")
572+
async def delete_cryptokey(
573+
server_id: str, zone_id: str, cryptokey_id: str, X_API_Key: str = Header()
574+
):
575+
"""
576+
This method deletes a key specified by cryptokey_id.
577+
578+
<https://doc.powerdns.com/authoritative/http-api/cryptokey.html#delete--servers-server_id-zones-zone_id-cryptokeys-cryptokey_id>
579+
"""
580+
environment = get_environment_for_token(config, X_API_Key)
581+
if not check_pdns_cryptokeys_allowed(environment, zone_id):
582+
logger.info(f"CryptoKeys not allowed for environment {environment.name}")
583+
raise ZoneNotAllowedException()
584+
resp = await pdns.delete(
585+
f"/api/v1/servers/{server_id}/zones/{zone_id}/cryptokeys/{cryptokey_id}"
586+
)
587+
pdns_response = await handle_pdns_response(resp)
588+
status_code = pdns_response.raise_for_error()
589+
return JSONResponse(content=pdns_response.data, status_code=status_code)
590+
591+
482592
@router_pdns.get("/servers/{server_id}/tsigkeys")
483593
async def list_tsigkeys(server_id: str, X_API_Key: str = Header()):
484594
"""

tests/unit/config_test.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from powerdns_api_proxy.config import (
88
check_acme_record_allowed,
99
check_pdns_search_allowed,
10+
check_pdns_cryptokeys_allowed,
1011
check_pdns_tsigkeys_allowed,
1112
check_pdns_zone_admin,
1213
check_pdns_zone_allowed,
@@ -225,7 +226,7 @@ def test_check_pdns_zone_allowed_allowed_without_trailing_point():
225226

226227

227228
def test_check_pdns_zone_allowed_allowed_without_trailing_point_point_last_item():
228-
env = dummy_proxy_environment
229+
env = deepcopy(dummy_proxy_environment)
229230
env.zones[0].name = "blablub.example.com+"
230231
zone = "blablub.example.com"
231232
assert not check_pdns_zone_allowed(env, zone)
@@ -585,6 +586,26 @@ def test_search_allowed_globally():
585586
assert check_pdns_search_allowed(environment, "test", "all") is True
586587

587588

589+
def test_cryptokeys_not_allowed():
590+
environment = dummy_proxy_environment
591+
assert check_pdns_cryptokeys_allowed(environment, "test") is False
592+
assert check_pdns_cryptokeys_allowed(environment, "test.example.com.") is False
593+
594+
595+
def test_cryptokeys_allowed_zone_only():
596+
environment = deepcopy(dummy_proxy_environment)
597+
environment.zones[0].cryptokeys = True
598+
assert check_pdns_cryptokeys_allowed(environment, "test") is False
599+
assert check_pdns_cryptokeys_allowed(environment, "test.example.com.") is True
600+
601+
602+
def test_cryptokeys_allowed_global():
603+
environment = deepcopy(dummy_proxy_environment)
604+
environment.global_cryptokeys = True
605+
assert check_pdns_cryptokeys_allowed(environment, "test") is True
606+
assert check_pdns_cryptokeys_allowed(environment, "test.example.com.") is True
607+
608+
588609
def test_tsigkeys_not_allowed():
589610
environment = deepcopy(dummy_proxy_environment)
590611
environment.global_tsigkeys = False

0 commit comments

Comments
 (0)