New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ feat: implements APIs for managing ACLs #1792
base: main
Are you sure you want to change the base?
Conversation
4331948
to
a536ecb
Compare
a536ecb
to
0b3660c
Compare
I might have some free time this evening to do a review |
I've attached a screen recording to the PR in case someone wishes to see how the APIs work. |
Headscale currently lacks the APIs to manange the ACLs. The only way possible currently is to load the ACLs via file and changes to the policy requires reloading the headscale process. This also makes it difficult to integrate your headscale via APIs with no ACL management. This commit introduces two APIs that allow your to get and set the policy.
0b3660c
to
6099d3b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall it looks good - I've left some inline comments. One open question is whether to support reading the current ACL via the new APIs/CLI commands even if the policy is backed by a file (for convenience)?
hscontrol/types/acl.go
Outdated
Policy datatypes.JSON | ||
|
||
CreatedAt *time.Time | ||
Expiration *time.Time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Expiration
and LastSeen
copy pasta? :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. 😅 I'll fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GORM also adds created at I believe, so that can also be omitted
@@ -180,6 +189,8 @@ func LoadConfig(path string, isFile bool) error { | |||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) | |||
viper.AutomaticEnv() | |||
|
|||
viper.SetDefault("acl.policy_mode", ACLPolicyModeDB) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this break existing setups which already uses a file? I'm not a maintainer, but I would be fine with the DB being the default going forward but in that case we should not break existing users setups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this will be a breaking change. I can set up an alias for existing users. However, their setups may already be broken from the recent database config restructure. I'm of the opinion that we need not keep it backward compatible since users will have to update their configs anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment earlier, I think file should still be the default. I'm open to be convinced.
return nil, types.ErrACLPolicyUpdateIsDisabled | ||
} | ||
|
||
polBytes, err := request.GetPolicy().MarshalJSON() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this fail if the input is HuJSON (default Tailscale ACL format)? policy.LoadACLPolicyFromBytes
will use the hujson parser by default when the format is not yaml
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bringing this up. I will test and verify it.
hscontrol/grpcv1.go
Outdated
return nil, errors.Wrap(err, types.ErrInvalidACLPolicyFormat.Error()) | ||
} | ||
|
||
api.h.ACLPolicy = a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this line be moved down to after the ACL is successfully persisted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I've moved it after we set the ACL in the database.
@@ -761,25 +768,9 @@ func (h *Headscale) Serve() error { | |||
Msg("Received SIGHUP, reloading ACL and Config") | |||
|
|||
// TODO(kradalby): Reload config on SIGHUP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// TODO(kradalby): Reload config on SIGHUP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still isn't implemented. I'll keep this for the time being.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably not allow "large" TODOs like this without an issue... (including me)
hscontrol/app.go
Outdated
h.ACLPolicy = pol | ||
|
||
log.Info(). | ||
Msg("ACL policy successfully reloaded, notifying nodes of change") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Msg("ACL policy successfully reloaded, notifying nodes of change") | |
Msg("ACL policy successfully loaded, notifying nodes of change") |
This function is both used for the initial loading and for the SIGHUP reloading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the log close to the notify call.
hscontrol/app.go
Outdated
} | ||
|
||
if h.ACLPolicy != nil { | ||
ctx := types.NotifyCtx(context.Background(), "acl-sighup", "na") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to notifyCtx + notifyAll if it's not a reload, but a normal startup? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not. 😅 Thanks, I've updated this bit.
Enabled it. 👍🏼 Now it will work for both modes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks reasonable, some comments and questions and to larger things:
I am tempted to use this opportunity to change the general phrasing from ACL to Policy through out the code, particularly since we are changing the config, adding commands etc.
I think the base format for the policy should be HuJSON, it is a bit more flexible format that allows preserving comments. This probably means using string/byte for the database and not the JSON type, but we dont really use it (or will use SQL to look up in the JSON) so that should not be a problem.
As part of this, I am willing to discuss dropping the YAML support to just streamline what we support.
|
||
var getACL = &cobra.Command{ | ||
Use: "get", | ||
Short: "Print the current ACL Policy JSON", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A very high level note, I think this should support the same HuJSON (and potentially YAML) format that the ACL support.
The main reason for this is that it can preserve comments which would be important for large policies.
I would also assume that some of the WebUIs will implement some sort of editors, and having comments would be important for documentation purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record, I am OK with just HuJSON, we might want to remove the YAML support and only support one format.
# If "file" is selected, the policies will be loaded from a file | ||
# located at `policy_path`. It will not be stored in the databse | ||
# and you will not be able to get or set the ACL via the ACL APIs. | ||
policy_mode: "db" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open for the example config to be db
, but the default should still be file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open for the example config to be db, but the default should still be file.
Personally I strongly disagree.
-
Many people still use docker. (I know I know, it's not officially supported... still!)
Manipulating files inside it, especially if the container is not running is a very hard task. -
IMHO it is more secure to store ACL data inside a
db
. A separate file can be easier hacked / modified / viewed.
After all, ACL is the "main security" config of HeadScale, telling who has access to what !
Please keep in mind: The main reason for using VPNs is security.
Offering an easy-to-view / edit file to the open is against it. -
Eventually more people will use this feature using a web-UI, so it would make much easier to setup with that already.
-
For those who like to do things manually (by typing text files, etc) -> changing the config to
file
will not be a big task to do, but for those who are not used to do tasks like this: it would be an "impossible nightmare".
Anyway: is the database password protected by default, so only HS can access it?
- or can other programs access it too?
@@ -96,6 +98,7 @@ require ( | |||
github.com/glebarez/go-sqlite v1.22.0 // indirect | |||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect | |||
github.com/go-ole/go-ole v1.3.0 // indirect | |||
github.com/go-sql-driver/mysql v1.7.0 // indirect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder why we suddenly need a mysql driver?
@@ -761,25 +768,9 @@ func (h *Headscale) Serve() error { | |||
Msg("Received SIGHUP, reloading ACL and Config") | |||
|
|||
// TODO(kradalby): Reload config on SIGHUP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably not allow "large" TODOs like this without an issue... (including me)
aclPath := util.AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath) | ||
pol, err := policy.LoadACLPolicyFromPath(aclPath) | ||
if err != nil { | ||
return pkgerrors.Wrap(err, "failed to load ACL policy from file") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the difference between this error wrap compared to:
fmt.Errorf("failed to load ACL policy from file: %w", err)
hscontrol/types/acl.go
Outdated
Policy datatypes.JSON | ||
|
||
CreatedAt *time.Time | ||
Expiration *time.Time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GORM also adds created at I believe, so that can also be omitted
@@ -180,6 +189,8 @@ func LoadConfig(path string, isFile bool) error { | |||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) | |||
viper.AutomaticEnv() | |||
|
|||
viper.SetDefault("acl.policy_mode", ACLPolicyModeDB) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment earlier, I think file should still be the default. I'm open to be convinced.
policyPath := viper.GetString("acl.policy_path") | ||
policyMode := viper.GetString("acl.policy_mode") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should just use the term policy?
policy:
mode: "" // <-- does "source" or "storage" or "backend" make more sense than mode?
path: ""
) | ||
|
||
// ACL describes the data model for ACLs used to restrict access to resources. | ||
type ACL struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a proposal of having the policy versioned. I do not think we should do that now, but I think we should think of if there is anything about the format that should be addressed to enable that in the future. I do not have any concerns per now, but want to hear what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having the policy versioned.
I agree. Having plus one line of a simple version=2024-03-24
is not a big deal now,
but can save us lots of trouble later.
_(In my 30y. of experience I've realized:
- it is better to use ISO-dates for versions.
Saves lot's of time and prevents confusion.
Also better to merge with release-notes / what changed.)_
Thanks for the review. This sounds like a good idea. I wrote this implementation to quickly demonstrate the APIs and commands that we can use as a reference to discuss the feature further. Keeping the base format as I am also okay with keeping the default mode as Let me update this PR with the suggestions and put it our for a second round of review. |
Hi @pallabpain. Have you had a chance to update the PR yet? |
I haven't had the chance to update the PR, yet. I have some untested changes locally that I should be able to work on this weekend. I may change the request-response structure based on the newer comments. |
Maybe, we should create 2 new tables to store groups and tags. And sometimes, we just want to update groups or tags. So I'm thinking about how to update ACL groups automatically, e.g. AD/LDAP or extract from OIDC (OIDC may be a problem because it can only get groups when users authenticate) |
@pallabpain there has been quite some changes, do you think that you might have time to pick this up again? |
Sure. I'll review the recent changes and update the pull request. I'll try and get a working version ready soon. |
Description
Headscale currently lacks the APIs to manange the ACLs. The only way possible currently is to load the ACLs via file and changes to the policy requires reloading the headscale process. This also makes it difficult to integrate your headscale via APIs with no ACL management.
This commit introduces two APIs that allow your to get and set the policy.
APIs
Set ACL Policy
Payload (Example only)
Get ACL Policy
Response
CLI
Testing
acls_demo.mp4
Resolves