Skip to content

Commit 5cde747

Browse files
authored
Merge pull request #173 from tacerus/cryptokeys
feat(dnssec): support cryptokeys endpoint
2 parents 1cf7f02 + 43d8933 commit 5cde747

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
@@ -26,6 +26,7 @@ class ProxyConfigZone(BaseModel):
2626
`admin` enabled creating and deleting the zone.
2727
`subzones` sets the same permissions on all subzones.
2828
`all_records` will be set to `True` if no `records` are defined.
29+
`cryptokeys` enables management of DNSSEC.
2930
`read_only` controls write permissions for this specific zone.
3031
"""
3132

@@ -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
@@ -10,6 +10,7 @@
1010

1111
from powerdns_api_proxy.config import (
1212
check_pdns_search_allowed,
13+
check_pdns_cryptokeys_allowed,
1314
check_pdns_tsigkeys_allowed,
1415
check_pdns_zone_admin,
1516
check_pdns_zone_allowed,
@@ -480,6 +481,115 @@ async def search_data(
480481
return JSONResponse(content=pdns_response.data, status_code=status_code)
481482

482483

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

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)