Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1911dd38 | ||
|
|
310ed8061e |
No files matched your search
@@ -6,6 +6,15 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.6.2] - 2026-10-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A `certificates` role (template): `certificates:write`, for tokens and
|
||||||
|
users that get certificates for other hosts with `jarl cert get`. In
|
||||||
|
the web UI's user and token forms it is in "Insert a template…", to
|
||||||
|
narrow with `names=`; in the CLI, `--role certificates`.
|
||||||
|
|
||||||
## [0.6.1] - 2026-10-20
|
## [0.6.1] - 2026-10-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -202,7 +202,8 @@ review step.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Version 0.6.1 is the current release. It gets certificates onto your
|
Version 0.6.2 is the current release. It adds a `certificates` role
|
||||||
|
for tokens that get certificates for other hosts. 0.6.1 gets certificates onto your
|
||||||
other hosts (`jarl cert get`: Jarl obtains and renews them, a host downloads them and runs a
|
other hosts (`jarl cert get`: Jarl obtains and renews them, a host downloads them and runs a
|
||||||
hook, a daily timer keeps them current), exports certificates, fixes
|
hook, a daily timer keeps them current), exports certificates, fixes
|
||||||
PTR records and overrides in linked zones, and makes one-view tokens
|
PTR records and overrides in linked zones, and makes one-view tokens
|
||||||
|
|||||||
@@ -392,7 +392,8 @@ ACCESS is read or write, and filters take comma separated glob patterns:
|
|||||||
records:write zones=adbraun.lol names=_acme-challenge.* types=TXT
|
records:write zones=adbraun.lol names=_acme-challenge.* types=TXT
|
||||||
*:read
|
*:read
|
||||||
|
|
||||||
Roles are ready-made grant sets: readonly, dns-editor, operator, acme.`
|
Roles are ready-made grant sets: readonly, dns-editor, operator, acme,
|
||||||
|
certificates.`
|
||||||
|
|
||||||
func userCmd() *cobra.Command {
|
func userCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{Use: "user", Aliases: []string{"users"}, Short: "Manage users (admin)", Long: "Manage users (admin).\n\n" + grantHelp}
|
cmd := &cobra.Command{Use: "user", Aliases: []string{"users"}, Short: "Manage users (admin)", Long: "Manage users (admin).\n\n" + grantHelp}
|
||||||
@@ -477,7 +478,7 @@ func userCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
addUserFlags := func(c *cobra.Command) {
|
addUserFlags := func(c *cobra.Command) {
|
||||||
c.Flags().BoolVar(&admin, "admin", false, "make the user an administrator (--admin=false to remove)")
|
c.Flags().BoolVar(&admin, "admin", false, "make the user an administrator (--admin=false to remove)")
|
||||||
c.Flags().StringVar(&role, "role", "", "replace the grants with a role: readonly, dns-editor, operator or acme")
|
c.Flags().StringVar(&role, "role", "", "replace the grants with a role: readonly, dns-editor, operator, acme or certificates")
|
||||||
c.Flags().StringArrayVar(&grants, "grant", nil, `grant (repeatable), e.g. "records:write zones=example.com"`)
|
c.Flags().StringArrayVar(&grants, "grant", nil, `grant (repeatable), e.g. "records:write zones=example.com"`)
|
||||||
c.Flags().StringVar(&comment, "comment", "", "comment")
|
c.Flags().StringVar(&comment, "comment", "", "comment")
|
||||||
c.Flags().IntVar(&certLimit, "cert-limit", -1, "how many certificates the user may own through jarl cert get (-1: certificates.max_per_user in jarl.yaml)")
|
c.Flags().IntVar(&certLimit, "cert-limit", -1, "how many certificates the user may own through jarl cert get (-1: certificates.max_per_user in jarl.yaml)")
|
||||||
@@ -758,7 +759,7 @@ func tokenCmd() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
create.Flags().StringVar(&user, "user", "", "create the token for this user (admin)")
|
create.Flags().StringVar(&user, "user", "", "create the token for this user (admin)")
|
||||||
create.Flags().StringVar(&role, "role", "", "restrict the token to a role: readonly, dns-editor, operator or acme")
|
create.Flags().StringVar(&role, "role", "", "restrict the token to a role: readonly, dns-editor, operator, acme or certificates")
|
||||||
create.Flags().StringArrayVar(&grants, "grant", nil, "restrict the token to these grants (repeatable)")
|
create.Flags().StringArrayVar(&grants, "grant", nil, "restrict the token to these grants (repeatable)")
|
||||||
create.Flags().StringVar(&expires, "expires", "", "lifetime, e.g. 90d or 720h (default: never)")
|
create.Flags().StringVar(&expires, "expires", "", "lifetime, e.g. 90d or 720h (default: never)")
|
||||||
create.Flags().BoolVar(&save, "save", false, "store the new token in the CLI configuration file")
|
create.Flags().BoolVar(&save, "save", false, "store the new token in the CLI configuration file")
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ jarl token list --all --expiring [60d] # expiring within 14 days
|
|||||||
jarl token create laptop --expires 365d
|
jarl token create laptop --expires 365d
|
||||||
jarl token create backup-script --grant "*:read"
|
jarl token create backup-script --grant "*:read"
|
||||||
jarl token create acme --user certbot --role acme
|
jarl token create acme --user certbot --role acme
|
||||||
|
jarl token create web01 --grant "certificates:write names=web01.adbraun.lol" # jarl cert get on one host (or --role certificates)
|
||||||
jarl token rotate laptop --save # new secret, same token and grants
|
jarl token rotate laptop --save # new secret, same token and grants
|
||||||
jarl token rotate --current --save --grace 10m # the token this CLI uses
|
jarl token rotate --current --save --grace 10m # the token this CLI uses
|
||||||
jarl token revoke 7
|
jarl token revoke 7
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ you can then edit.
|
|||||||
| `dns-editor` | `zones:write`, `records:write`, `views:read`, `acls:read`, `options:read`, `blocking:read` |
|
| `dns-editor` | `zones:write`, `records:write`, `views:read`, `acls:read`, `options:read`, `blocking:read` |
|
||||||
| `operator` | `*:write` (everything except user management) |
|
| `operator` | `*:write` (everything except user management) |
|
||||||
| `acme` | `records:write names=_acme-challenge.* types=TXT` |
|
| `acme` | `records:write names=_acme-challenge.* types=TXT` |
|
||||||
|
| `certificates` | `certificates:write`: certificates for other hosts (`jarl cert get`) for any name in Jarl's zones. Narrow it to a host's names, e.g. `certificates:write names=web01.adbraun.lol` ([certificates.md](certificates.md#certificates-for-other-hosts)) |
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
jarl user create alice --role dns-editor
|
jarl user create alice --role dns-editor
|
||||||
@@ -210,6 +211,7 @@ The audit log shows the user and the token name.
|
|||||||
jarl token create laptop # full rights of the user, no expiry
|
jarl token create laptop # full rights of the user, no expiry
|
||||||
jarl token create ci --grant "records:write zones=lab.adbraun.lol" --expires 90d
|
jarl token create ci --grant "records:write zones=lab.adbraun.lol" --expires 90d
|
||||||
jarl token create certbot --user certbot --role acme # admin: a token for another user
|
jarl token create certbot --user certbot --role acme # admin: a token for another user
|
||||||
|
jarl token create web01 --grant "certificates:write names=web01.adbraun.lol" # jarl cert get on one host
|
||||||
jarl token list --all
|
jarl token list --all
|
||||||
jarl token revoke 12
|
jarl token revoke 12
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -135,6 +135,16 @@ func TestRoleTemplatesValid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCertificatesRole(t *testing.T) {
|
||||||
|
p := &Principal{User: &model.User{Grants: RoleTemplates["certificates"]}}
|
||||||
|
if !p.Can(Action{Resource: ResCerts, Write: true, Name: "web01.adbraun.lol"}) {
|
||||||
|
t.Error("the certificates role can't get a certificate")
|
||||||
|
}
|
||||||
|
if p.Can(Action{Resource: ResRecords, Write: true, Zone: "adbraun.lol."}) || p.Can(Action{Resource: ResZones, Zone: "adbraun.lol."}) {
|
||||||
|
t.Error("the certificates role reaches beyond certificates")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCertificateGrants(t *testing.T) {
|
func TestCertificateGrants(t *testing.T) {
|
||||||
g, err := ParseGrant("certificates:write names=*.adbraun.lol")
|
g, err := ParseGrant("certificates:write names=*.adbraun.lol")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -391,4 +391,7 @@ var RoleTemplates = map[string][]model.Grant{
|
|||||||
},
|
},
|
||||||
"operator": {{Resource: ResAll, Access: Write}},
|
"operator": {{Resource: ResAll, Access: Write}},
|
||||||
"acme": {{Resource: ResRecords, Access: Write, Names: []string{"_acme-challenge.*"}, Types: []string{"TXT"}}},
|
"acme": {{Resource: ResRecords, Access: Write, Names: []string{"_acme-challenge.*"}, Types: []string{"TXT"}}},
|
||||||
|
// certificates for other hosts (jarl cert get): narrow it with names=
|
||||||
|
// to the host's own.
|
||||||
|
"certificates": {{Resource: ResCerts, Access: Write}},
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,8 @@ async function rotateDialog(t, redraw) {
|
|||||||
function coveredBy(user, grant) {
|
function coveredBy(user, grant) {
|
||||||
if (!user || user.admin) return true;
|
if (!user || user.admin) return true;
|
||||||
const [res, acc] = grant.split(/\s+/)[0].split(":");
|
const [res, acc] = grant.split(/\s+/)[0].split(":");
|
||||||
return (user.grants || []).some((g) => (g.resource === "*" || g.resource === res) && (g.access === "write" || acc === "read"));
|
// "*" never covers certificates (they hand out private keys).
|
||||||
|
return (user.grants || []).some((g) => ((g.resource === "*" && res !== "certificates") || g.resource === res) && (g.access === "write" || acc === "read"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTokenForm makes a token for owner (the user it belongs to). Its
|
// newTokenForm makes a token for owner (the user it belongs to). Its
|
||||||
|
|||||||
Reference in new issue
Block a user